From 70aa0ddaf4a1e57daccb10797d3afee433f174f6 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Mon, 22 Dec 2014 08:38:00 +0000 Subject: [PATCH] Enable Material theme for Android With this CL, Mozc for Android supports Material theme. Note that this CL also contains a lot of other improvements and bug fixes that might not be directly related to the Material theme. Here are some examples: * Floating candidate window support in Android 5.0. * Improved accessibility support. * Start bundling Key Character Map (KCM) file of Japanese 109 keyboard. See the release note for details. As for desktop versions, no behavior change is intended. BUG=none TEST=manually done with Nexus 5 / Android 5.0.1 (LRX22C) git-svn-id: https://mozc.googlecode.com/svn/trunk@467 a6090854-d499-a067-5803-1114d4e51264 --- src/DEPS | 4 + src/android/AndroidManifest_template.xml | 49 +- src/android/android.gyp | 35 +- src/android/android_env.gypi | 2 +- src/android/android_resources.gypi | 60 + src/android/gen_mozc_drawable.py | 384 +++-- src/android/gen_subset_font.py | 139 ++ src/android/proguard-project.txt | 5 + src/android/project.properties | 4 +- src/android/protobuf/project.properties | 2 +- .../AndroidManifest.xml | 2 +- .../ant.properties | 0 .../resources_oss => resources}/build.xml | 0 src/android/resources/proguard-project.txt | 49 + .../project.properties | 2 +- src/android/resources/resources.gyp | 304 ++++ .../resources/src/DONT_REMOVE_THIS_DIRECTORY | 0 .../ApplicationInitializerFactory.java | 125 +- .../inputmethod/japanese/CandidateView.java | 115 +- .../japanese/CandidateViewManager.java | 456 ++++++ .../japanese/CandidateWordView.java | 117 +- .../ComposingTextTrackingInputConnection.java | 5 + .../ConversionCandidateWordContainerView.java | 3 +- .../japanese/DependencyFactory.java | 1 + .../inputmethod/japanese/FeedbackManager.java | 30 +- .../japanese/FloatingCandidateView.java | 550 +++++++ .../japanese/InputDeviceReceiver.java | 39 + .../japanese/JapaneseKeyboard.java | 323 ----- .../japanese/JapaneseKeyboardParser.java | 80 -- .../japanese/KeyEventButtonTouchListener.java | 20 +- .../japanese/KeycodeConverter.java | 15 +- .../japanese/LauncherActivity.java} | 38 +- ...=> LauncherIconVisibilityInitializer.java} | 47 +- .../japanese/MozcMenuDialogListenerImpl.java | 12 +- .../inputmethod/japanese/MozcService.java | 566 +++++--- .../inputmethod/japanese/MozcUtil.java | 180 ++- .../inputmethod/japanese/MozcView.java | 625 ++++---- .../inputmethod/japanese/NarrowFrameView.java | 170 +++ .../japanese/PrimaryKeyCodeConverter.java | 50 +- .../inputmethod/japanese/SymbolInputView.java | 921 +++++++----- .../japanese/ViewEventDelegator.java | 39 +- .../japanese/ViewEventListener.java | 34 +- .../inputmethod/japanese/ViewManager.java | 622 ++++++-- .../japanese/ViewManagerInterface.java | 46 +- ...didateWindowAccessibilityNodeProvider.java | 5 + .../KeyboardAccessibilityDelegate.java | 44 +- .../KeyboardAccessibilityNodeProvider.java | 5 +- .../hardwarekeyboard/HardwareKeyboard.java | 2 +- .../HardwareKeyboardSpecification.java | 20 +- .../KeyEventMapperFactory.java | 20 - .../keyboard/BackgroundDrawableFactory.java | 581 ++++---- .../inputmethod/japanese/keyboard/Flick.java | 12 +- .../inputmethod/japanese/keyboard/Key.java | 37 +- .../japanese/keyboard/KeyEntity.java | 76 +- .../japanese/keyboard/KeyEventContext.java | 117 +- .../japanese/keyboard/KeyEventHandler.java | 70 +- .../japanese/keyboard/KeyState.java | 54 +- .../japanese/keyboard/Keyboard.java | 275 +++- .../keyboard/KeyboardActionListener.java | 4 +- .../KeyboardFactory.java} | 32 +- .../japanese/keyboard/KeyboardParser.java | 653 ++++++--- .../japanese/keyboard/KeyboardView.java | 438 +++--- .../KeyboardViewBackgroundSurface.java | 246 ++-- .../inputmethod/japanese/keyboard/PopUp.java | 25 +- .../japanese/keyboard/PopUpPreview.java | 125 +- .../keyboard/ProbableKeyEventGuesser.java | 96 +- .../inputmethod/japanese/keyboard/Row.java | 4 +- .../model/JapaneseSoftwareKeyboardModel.java | 81 +- .../model/SymbolCandidateStorage.java | 4 + .../japanese/model/SymbolMajorCategory.java | 32 +- .../japanese/model/SymbolMinorCategory.java | 71 +- .../preference/ClientSidePreference.java | 25 +- .../preference/KeyboardLayoutPreference.java | 38 +- .../preference/KeyboardPreviewDrawable.java | 102 +- .../preference/MiniBrowserActivity.java | 47 +- .../MozcBasePreferenceActivity.java | 8 +- .../MozcFragmentPreferenceActivity.java | 34 + .../japanese/preference/PreferenceUtil.java | 64 +- .../japanese/session/LocalSessionHandler.java | 6 +- .../japanese/session/MozcCommandDebugger.java | 5 +- .../inputmethod/japanese/session/MozcJNI.java | 6 + .../japanese/session/SessionExecutor.java | 404 +++--- .../session/SessionHandlerFactory.java | 20 +- .../session/SocketSessionHandler.java | 5 +- .../japanese/ui/CandidateLayoutRenderer.java | 166 ++- .../ui/ConversionCandidateLayouter.java | 17 +- .../ui/FloatingCandidateLayoutRenderer.java | 500 +++++++ .../japanese/ui/FloatingModeIndicator.java | 247 ++++ .../japanese/ui/InputFrameFoldButtonView.java | 123 ++ .../inputmethod/japanese/ui/MenuDialog.java | 16 +- .../japanese/ui/PopUpLayouter.java | 120 ++ .../japanese/ui/ScrollGuideView.java | 24 +- .../japanese/ui/SideFrameStubProxy.java | 110 +- .../inputmethod/japanese/ui/SpanFactory.java | 45 +- .../UserDictionaryActionBarHelperFactory.java | 188 --- .../UserDictionaryToolActivity.java | 78 +- .../userdictionary/UserDictionaryUtil.java | 85 +- .../util/CandidateDescriptionUtil.java | 139 ++ .../japanese/util/ImeSwitcherFactory.java | 43 +- .../util/LauncherIconManagerFactory.java | 139 ++ .../inputmethod/japanese/util/ParserUtil.java | 98 ++ .../japanese/view/DrawableCache.java | 26 +- .../japanese/view/DummyDrawable.java | 82 ++ .../japanese/view/LightIconDrawable.java | 116 -- .../japanese/view/MozcDrawableFactory.java | 233 ++- .../japanese/view/MozcImageButton.java | 94 ++ .../japanese/view/MozcImageCapableView.java | 174 +++ .../japanese/view/MozcImageView.java | 104 ++ .../japanese/view/MozcPictureDrawable.java | 19 + .../view/PopUpFrameWindowDrawable.java | 135 -- ...wable.java => QwertySpaceKeyDrawable.java} | 69 +- .../japanese/view/RoundRectKeyDrawable.java | 20 +- .../inputmethod/japanese/view/Skin.java | 336 +++++ .../inputmethod/japanese/view/SkinParser.java | 229 +++ .../inputmethod/japanese/view/SkinType.java | 775 +--------- ...bolMajorCategoryButtonDrawableFactory.java | 116 +- .../view/TabSelectedBackgroundDrawable.java | 4 +- .../drawable-hdpi/application_icon.png | Bin .../drawable-mdpi/application_icon.png | Bin 0 -> 3774 bytes .../drawable-xhdpi/application_icon.png | Bin .../drawable-xxhdpi/application_icon.png | Bin 0 -> 10508 bytes .../drawable-xxxhdpi/application_icon.png | Bin 0 -> 35940 bytes .../launcher_icon_preinstall_bools.xml} | 8 +- .../launcher_icon_standard_bools.xml} | 10 +- .../status_icon_image_alphabet.png | Bin 826 -> 452 bytes .../status_icon_image_hiragana.png | Bin 1024 -> 680 bytes .../drawable-hdpi/window__background_dark.png | Bin 0 -> 82 bytes .../status_icon_image_alphabet.png | Bin 590 -> 549 bytes .../status_icon_image_hiragana.png | Bin 1221 -> 813 bytes .../resources_oss/res/layout/button_frame.xml | 54 + .../res/layout/candidate_view.xml | 9 +- .../res/layout/first_time_launch.xml | 3 +- .../resources_oss/res/layout/left_frame.xml | 51 +- .../resources_oss/res/layout/mozc_view.xml | 380 ++--- .../resources_oss/res/layout/right_frame.xml | 53 +- .../res/layout/symbol_candidate_view.xml | 11 +- .../resources_oss/res/layout/symbol_view.xml | 258 +++- .../user_dictionary_tool_action_bar_view.xml | 76 - ...onary_tool_dictionary_name_dialog_view.xml | 1 - .../user_dictionary_tool_empty_scrollview.xml | 65 - .../res/layout/user_dictionary_tool_view.xml | 39 - ...tionary_tool_word_register_dialog_view.xml | 3 - .../res/menu/user_dictionary_tool_menu.xml | 4 +- .../res/values-h380dp-land/dimens.xml | 49 + .../config.xml} | 11 +- .../res/values-h500dp-land/dimens.xml | 101 ++ .../config.xml} | 10 +- .../res/values-h570dp-land/dimens.xml | 70 + .../res/values-h570dp-land/raws.xml | 150 ++ .../res/values-h650dp-port/dimens.xml | 54 + .../dimens.xml} | 36 +- .../res/values-h800dp-port/config.xml | 39 + .../res/values-h800dp-port/dimens.xml | 100 ++ .../res/values-h800dp-port/raws.xml | 150 ++ .../res/values-h900dp-port/dimens.xml | 69 + .../res/values-ja/contentdescription.xml | 61 +- .../resources_oss/res/values-ja/strings.xml | 44 +- .../resources_oss/res/values-land/dimens.xml | 21 +- .../resources_oss/res/values-land/strings.xml | 53 - .../resources_oss/res/values-v16/bools.xml | 36 + .../resources_oss/res/values-v21/strings.xml | 34 + .../resources_oss/res/values-v21/themes.xml | 37 + .../resources_oss/res/values/application.xml | 18 - .../resources_oss/res/values/arrays.xml | 14 - .../config.xml => values/arrays_skin.xml} | 26 +- .../resources_oss/res/values/attrs.xml | 60 +- .../resources_oss/res/values/bools.xml | 4 +- .../resources_oss/res/values/colors.xml | 21 +- .../resources_oss/res/values/config.xml | 27 +- .../res/values/contentdescription.xml | 351 +++-- .../resources_oss/res/values/dimens.xml | 142 +- .../resources_oss/res/values/keycodes.xml | 6 +- .../resources_oss/res/values/raws.xml | 153 ++ .../resources_oss/res/values/strings.xml | 523 +++---- .../resources_oss/res/values/themes.xml | 37 + .../res/values/untranslatable_application.xml | 55 + .../res/values/untranslatable_strings.xml | 87 ++ .../resources_oss/res/xml/kbd_123.xml | 381 +++++ .../resources_oss/res/xml/kbd_12keys_123.xml | 280 ---- .../resources_oss/res/xml/kbd_12keys_abc.xml | 223 ++- .../res/xml/kbd_12keys_flick_abc.xml | 418 ++++-- .../res/xml/kbd_12keys_flick_kana.xml | 359 +++-- .../resources_oss/res/xml/kbd_12keys_kana.xml | 240 +++- .../res/xml/kbd_12keys_qwerty_abc.xml | 1265 ----------------- .../resources_oss/res/xml/kbd_godan_kana.xml | 303 +++- .../resources_oss/res/xml/kbd_qwerty_abc.xml | 393 +++-- .../res/xml/kbd_qwerty_abc_123.xml | 280 ++-- .../resources_oss/res/xml/kbd_qwerty_kana.xml | 309 +++- .../res/xml/kbd_qwerty_kana_123.xml | 720 ---------- ...2keys_flick_123.xml => kbd_symbol_123.xml} | 245 ++-- .../res/xml/keyboard_layouts.xml | 36 + .../resources_oss/res/xml/method.xml | 3 +- .../resources_oss/res/xml/pref_about.xml | 6 + .../res/xml/pref_input_support.xml | 14 +- .../res/xml/skin_blue_darkgray.xml | 455 ++++++ .../res/xml/skin_blue_lightgray.xml | 461 ++++++ .../res/xml/skin_material_design_dark.xml | 451 ++++++ .../res/xml/skin_material_design_light.xml | 451 ++++++ .../res/xml/skin_orange_lightgray.xml | 461 ++++++ .../tests/AndroidManifest_template.xml | 2 +- src/android/tests/project.properties | 2 +- src/android/tests/res/values-land/dimens.xml | 36 + src/android/tests/res/values/dimens.xml | 36 + .../skinparser_correct_drawable_test_1.xml | 38 + .../skinparser_incorrect_drawable_test_1.xml | 39 + .../tests/res/xml/skinparser_test_1.xml | 33 + .../tests/res/xml/skinparser_test_2.xml | 34 + .../tests/res/xml/skinparser_test_3.xml | 34 + .../tests/res/xml/skinparser_test_4.xml | 38 + .../tests/res/xml/skinparser_test_5.xml | 34 + .../tests/res/xml/skinparser_test_6.xml | 38 + .../tests/res/xml/skinparser_test_7.xml | 36 + .../ApplicationInitializerFactoryTest.java | 362 +++-- .../japanese/CandidateViewManagerTest.java | 386 +++++ .../japanese/CandidateViewTest.java | 118 +- .../japanese/CandidateWordViewTest.java | 40 +- .../japanese/FloatingCandidateViewTest.java | 351 +++++ .../InOutAnimatedFrameLayoutTest.java | 1 - .../japanese/JapaneseKeyboardViewTest.java | 176 --- .../KeyEventButtonTouchListenerTest.java | 6 +- .../japanese/KeyEventScenarioTest.java | 99 +- .../MozcMenuDialogListenerImplTest.java | 6 +- .../inputmethod/japanese/MozcServiceTest.java | 442 +++--- .../inputmethod/japanese/MozcUtilTest.java | 209 +-- .../inputmethod/japanese/MozcViewTest.java | 543 +++---- .../japanese/NarrowFrameViewTest.java | 109 ++ .../japanese/PrimaryKeyCodeConverterTest.java | 30 +- .../japanese/SymbolInputViewTest.java | 46 +- .../inputmethod/japanese/ViewManagerTest.java | 498 +++++-- .../KeyboardAccessibilityDelegateTest.java | 63 +- ...KeyboardAccessibilityNodeProviderTest.java | 27 +- .../HardwareKeyboardSpecificationTest.java | 97 +- .../HardwareKeyboardTest.java | 25 +- .../BackgroundDrawableFactoryTest.java | 30 +- .../keyboard/KeyEventContextTest.java | 232 ++- .../keyboard/KeyEventHandlerTest.java | 48 +- .../japanese/keyboard/KeyTest.java | 50 +- .../KeyboardFactoryTest.java} | 18 +- .../japanese/keyboard/KeyboardParserTest.java | 249 ++-- .../KeyboardTest.java} | 35 +- .../KeyboardViewBackgroundSurfaceTest.java | 256 ++-- .../japanese/keyboard/KeyboardViewTest.java | 358 +++-- .../japanese/keyboard/PopUpPreviewTest.java | 92 +- .../keyboard/ProbableKeyEventGuesserTest.java | 86 +- .../JapaneseSoftwareKeyboardModelTest.java | 213 ++- .../model/SymbolMajorCategoryTest.java | 3 + .../japanese/mushroom/MushroomUtilTest.java | 3 +- ...ConversionHistoryDialogPreferenceTest.java | 11 +- ...learSymbolHistoryDialogPreferenceTest.java | 11 +- ...earUserDictionaryDialogPreferenceTest.java | 11 +- .../preference/ClientSidePreferenceTest.java | 9 +- .../preference/MiniBrowserActivityTest.java | 6 +- .../preference/PreferenceUtilTest.java | 3 +- .../japanese/session/SessionExecutorTest.java | 179 ++- .../session/SessionHandlerFactoryTest.java | 16 +- .../japanese/stresstest/MozcStressTest.java | 32 +- .../japanese/testing/MainThreadRunner.java | 9 +- .../japanese/testing/MockContext.java | 45 + .../japanese/testing/MockDrawable.java | 57 + .../japanese/testing/MockInputStream.java | 44 + .../japanese/testing/MockPackageManager.java | 474 ++++++ .../MockResourcesWithDisplayMetrics.java | 46 + .../japanese/testing/MockWindow.java | 260 ++++ .../japanese/testing/MozcMatcher.java | 74 +- .../ui/CandidateLayoutRendererTest.java | 276 +++- .../ui/ConversionCandidateLayouterTest.java | 2 +- .../FloatingCandidateLayoutRendererTest.java | 186 +++ .../ui/FloatingModeIndicatorTest.java | 114 ++ .../japanese/ui/MenuDialogTest.java | 52 +- .../japanese/ui/PopUpLayouterTest.java | 97 ++ .../japanese/ui/ScrollGuideViewTest.java | 9 +- .../japanese/ui/SpanFactoryTest.java | 39 - .../util/CandidateDescriptionUtilTest.java | 96 ++ .../util/LauncherIconManagerFactoryTest.java | 109 ++ .../japanese/util/ResourcesWrapperTest.java | 5 +- .../japanese/util/ZipFileUtilTest.java | 2 +- .../japanese/vectorgraphic/BitmapSaver.java | 71 + .../japanese/view/DrawableCacheTest.java | 20 +- .../view/MozcDrawableFactoryTest.java | 7 +- .../japanese/view/SkinParserTest.java | 94 ++ .../japanese/view/SkinTypeTest.java | 14 +- src/android/userfeedback/project.properties | 2 +- src/base/base.gyp | 5 +- src/build_mozc.py | 40 +- src/build_tools/copy_file.py | 120 +- src/build_tools/util.py | 23 + src/composer/table.cc | 30 +- src/composer/table_test.cc | 20 +- .../images/android/svg/emoji_disable_icon.svg | 8 +- ...oating_mode_indicator__alphabet_normal.svg | 42 + ... floating_mode_indicator__kana_normal.svg} | 22 +- .../svg/function__action_done__icon.svg | 41 + ...icon.svg => function__action_go__icon.svg} | 23 +- .../svg/function__action_next__icon.svg | 41 + .../svg/function__action_previous__icon.svg | 41 + ....svg => function__action_search__icon.svg} | 12 +- .../svg/function__action_send__icon.svg | 42 + .../android/svg/function__enter__icon.svg | 42 + .../android/svg/function__symbol__popup.svg | 44 + src/data/images/android/svg/globe.svg | 44 - src/data/images/android/svg/globe_keyicon.svg | 44 + .../android/svg/globe_qwerty_function.svg | 44 + .../svg/godan__function__undo__icon.svg | 43 - .../images/android/svg/godan__kana__14.svg | 33 +- .../svg/godan__kana__support__01_center.svg | 48 - .../svg/godan__kana__support__01_down.svg | 48 - .../svg/godan__kana__support__02_center.svg | 50 - .../svg/godan__kana__support__02_down.svg | 50 - .../svg/godan__kana__support__02_right.svg | 50 - .../svg/godan__kana__support__02_up.svg | 50 - .../svg/godan__kana__support__03_center.svg | 51 - .../svg/godan__kana__support__03_down.svg | 51 - .../svg/godan__kana__support__03_left.svg | 51 - .../svg/godan__kana__support__03_right.svg | 51 - .../svg/godan__kana__support__03_up.svg | 51 - .../svg/godan__kana__support__04_center.svg | 48 - .../svg/godan__kana__support__04_down.svg | 48 - .../svg/godan__kana__support__05_center.svg | 50 - .../svg/godan__kana__support__05_down.svg | 50 - .../svg/godan__kana__support__05_release.svg | 50 - .../svg/godan__kana__support__05_right.svg | 50 - .../svg/godan__kana__support__05_up.svg | 50 - .../svg/godan__kana__support__06_center.svg | 49 - .../svg/godan__kana__support__06_down.svg | 49 - .../svg/godan__kana__support__06_release.svg | 49 - .../svg/godan__kana__support__06_right.svg | 49 - .../svg/godan__kana__support__06_up.svg | 49 - .../svg/godan__kana__support__07_center.svg | 48 - .../svg/godan__kana__support__07_down.svg | 48 - .../svg/godan__kana__support__07_release.svg | 48 - .../svg/godan__kana__support__08_center.svg | 50 - .../svg/godan__kana__support__08_down.svg | 50 - .../svg/godan__kana__support__08_release.svg | 50 - .../svg/godan__kana__support__08_right.svg | 50 - .../svg/godan__kana__support__08_up.svg | 50 - .../svg/godan__kana__support__09_center.svg | 49 - .../svg/godan__kana__support__09_down.svg | 49 - .../svg/godan__kana__support__09_release.svg | 49 - .../svg/godan__kana__support__09_up.svg | 49 - .../svg/godan__kana__support__10_release.svg | 47 - .../svg/godan__kana__support__11_center.svg | 48 - .../svg/godan__kana__support__11_down.svg | 48 - .../svg/godan__kana__support__11_release.svg | 48 - .../svg/godan__kana__support__12_center.svg | 57 - .../svg/godan__kana__support__12_down.svg | 55 - .../svg/godan__kana__support__12_left.svg | 55 - .../svg/godan__kana__support__12_release.svg | 57 - .../svg/godan__kana__support__12_right.svg | 57 - .../svg/godan__kana__support__12_up.svg | 57 - .../svg/godan__kana__support__13_center.svg | 47 - .../svg/godan__kana__support__15_center.svg | 48 - .../svg/godan__kana__support__15_release.svg | 48 - .../svg/godan__kana__support__15_up.svg | 48 - .../svg/godan__kana__support__popup__01.svg | 44 - .../svg/godan__kana__support__popup__02.svg | 50 - .../svg/godan__kana__support__popup__03.svg | 53 - .../svg/godan__kana__support__popup__04.svg | 44 - .../svg/godan__kana__support__popup__05.svg | 50 - .../svg/godan__kana__support__popup__06.svg | 53 - .../svg/godan__kana__support__popup__07.svg | 44 - .../svg/godan__kana__support__popup__08.svg | 50 - .../svg/godan__kana__support__popup__09.svg | 53 - .../svg/godan__kana__support__popup__10.svg | 41 - .../svg/godan__kana__support__popup__11.svg | 53 - .../svg/godan__kana__support__popup__12.svg | 53 - .../svg/godan__kana__support__popup__13.svg | 41 - .../svg/godan__kana__support__popup__15.svg | 50 - .../android/svg/hardware__function__close.svg | 38 +- ... keyboard_fold_tab_background_default.svg} | 22 +- .../keyboard_fold_tab_background_scrolled.svg | 44 + .../android/svg/keyboard_fold_tab_down.svg | 23 +- .../android/svg/keyboard_fold_tab_up.svg | 23 +- src/data/images/android/svg/microphone.svg | 35 + .../images/android/svg/qwerty__caps_on.svg | 31 +- .../svg/qwerty__function__alphabet__icon.svg | 28 +- ... => qwerty__function__alt__alted_icon.svg} | 15 +- ...g => qwerty__function__alt__base_icon.svg} | 11 +- .../android/svg/qwerty__function__comma.svg | 11 +- .../svg/qwerty__function__commercial_at.svg | 11 +- .../svg/qwerty__function__cursor__icon.svg | 41 - .../svg/qwerty__function__delete__icon.svg | 40 - .../svg/qwerty__function__delete__popup.svg | 40 - .../svg/qwerty__function__enter__icon.svg | 39 - .../svg/qwerty__function__enter__popup.svg | 40 - ...=> qwerty__function__exclamation_mark.svg} | 11 +- .../svg/qwerty__function__full_stop.svg | 11 +- .../qwerty__function__ideographic_comma.svg | 11 +- ...werty__function__ideographic_full_stop.svg | 11 +- .../svg/qwerty__function__kana__icon.svg | 26 +- .../svg/qwerty__function__kana__popup.svg | 43 - .../qwerty__function__left_arrow__icon.svg | 44 + ...werty__function__number_alphabet__icon.svg | 28 +- ...erty__function__number_alphabet__popup.svg | 46 - .../qwerty__function__number_kana__icon.svg | 46 - .../qwerty__function__number_kana__popup.svg | 46 - ...vg => qwerty__function__question_mark.svg} | 11 +- ...> qwerty__function__right_arrow__icon.svg} | 22 +- .../android/svg/qwerty__function__solidus.svg | 11 +- .../svg/qwerty__function__space__icon.svg | 21 +- .../svg/qwerty__function__space__popup.svg | 39 - .../svg/qwerty__function__symbol__icon.svg | 38 +- .../svg/qwerty__function__symbol__popup.svg | 49 - .../qwerty__function__text_alphabet__icon.svg | 15 +- ...qwerty__function__text_alphabet__popup.svg | 42 - .../qwerty__function__text_kana__popup.svg | 42 - .../svg/qwerty__keyicon__black_star.svg | 39 - .../qwerty__keyicon__circumflex_accent.svg | 39 - .../android/svg/qwerty__keyicon__cjk_day.svg | 39 - .../android/svg/qwerty__keyicon__cjk_hour.svg | 39 - .../svg/qwerty__keyicon__cjk_minute.svg | 39 - .../svg/qwerty__keyicon__cjk_month.svg | 39 - .../android/svg/qwerty__keyicon__colon.svg | 39 - .../android/svg/qwerty__keyicon__comma.svg | 39 - .../svg/qwerty__keyicon__commercial_at.svg | 39 - .../svg/qwerty__keyicon__digit_eight.svg | 39 - .../svg/qwerty__keyicon__digit_five.svg | 39 - .../svg/qwerty__keyicon__digit_four.svg | 39 - .../svg/qwerty__keyicon__digit_nine.svg | 39 - .../svg/qwerty__keyicon__digit_one.svg | 39 - .../svg/qwerty__keyicon__digit_seven.svg | 39 - .../svg/qwerty__keyicon__digit_six.svg | 39 - .../svg/qwerty__keyicon__digit_three.svg | 39 - .../svg/qwerty__keyicon__digit_two.svg | 39 - .../svg/qwerty__keyicon__digit_zero.svg | 39 - .../svg/qwerty__keyicon__division_sign.svg | 39 - .../svg/qwerty__keyicon__dollar_sign.svg | 39 - .../svg/qwerty__keyicon__eighth_note.svg | 39 - .../svg/qwerty__keyicon__equals_sign.svg | 39 - .../svg/qwerty__keyicon__exclamation_mark.svg | 39 - .../svg/qwerty__keyicon__full_stop.svg | 39 - .../svg/qwerty__keyicon__fullwidth_colon.svg | 39 - ...qwerty__keyicon__fullwidth_equals_sign.svg | 39 - ...y__keyicon__fullwidth_exclamation_mark.svg | 39 - ..._keyicon__fullwidth_left_curly_bracket.svg | 39 - ...y__keyicon__fullwidth_left_parenthesis.svg | 39 - .../qwerty__keyicon__fullwidth_plus_sign.svg | 39 - ...erty__keyicon__fullwidth_question_mark.svg | 39 - ...keyicon__fullwidth_right_curly_bracket.svg | 39 - ...__keyicon__fullwidth_right_parenthesis.svg | 39 - .../qwerty__keyicon__fullwidth_semicolon.svg | 39 - .../qwerty__keyicon__fullwidth_solidus.svg | 39 - .../svg/qwerty__keyicon__grave_accent.svg | 39 - .../qwerty__keyicon__greater_than_sign.svg | 39 - .../qwerty__keyicon__horizontal_ellipsis.svg | 39 - .../svg/qwerty__keyicon__hyphen_minus.svg | 39 - .../qwerty__keyicon__ideographic_comma.svg | 39 - ...qwerty__keyicon__ideographic_full_stop.svg | 39 - .../qwerty__keyicon__katakana_middle_dot.svg | 39 - ...werty__keyicon__latin_capital_letter_a.svg | 39 - ...werty__keyicon__latin_capital_letter_b.svg | 39 - ...werty__keyicon__latin_capital_letter_c.svg | 39 - ...werty__keyicon__latin_capital_letter_d.svg | 39 - ...werty__keyicon__latin_capital_letter_e.svg | 39 - ...werty__keyicon__latin_capital_letter_f.svg | 39 - ...werty__keyicon__latin_capital_letter_g.svg | 39 - ...werty__keyicon__latin_capital_letter_h.svg | 40 - ...werty__keyicon__latin_capital_letter_i.svg | 39 - ...werty__keyicon__latin_capital_letter_j.svg | 39 - ...werty__keyicon__latin_capital_letter_k.svg | 39 - ...werty__keyicon__latin_capital_letter_l.svg | 39 - ...werty__keyicon__latin_capital_letter_m.svg | 39 - ...werty__keyicon__latin_capital_letter_n.svg | 39 - ...werty__keyicon__latin_capital_letter_o.svg | 39 - ...werty__keyicon__latin_capital_letter_p.svg | 39 - ...werty__keyicon__latin_capital_letter_q.svg | 39 - ...werty__keyicon__latin_capital_letter_r.svg | 39 - ...werty__keyicon__latin_capital_letter_s.svg | 39 - ...werty__keyicon__latin_capital_letter_t.svg | 39 - ...werty__keyicon__latin_capital_letter_u.svg | 39 - ...werty__keyicon__latin_capital_letter_v.svg | 39 - ...werty__keyicon__latin_capital_letter_w.svg | 39 - ...werty__keyicon__latin_capital_letter_x.svg | 39 - ...werty__keyicon__latin_capital_letter_y.svg | 39 - ...werty__keyicon__latin_capital_letter_z.svg | 40 - .../qwerty__keyicon__latin_small_letter_a.svg | 39 - .../qwerty__keyicon__latin_small_letter_b.svg | 39 - .../qwerty__keyicon__latin_small_letter_c.svg | 39 - .../qwerty__keyicon__latin_small_letter_d.svg | 39 - .../qwerty__keyicon__latin_small_letter_e.svg | 39 - .../qwerty__keyicon__latin_small_letter_f.svg | 39 - .../qwerty__keyicon__latin_small_letter_g.svg | 39 - .../qwerty__keyicon__latin_small_letter_h.svg | 39 - .../qwerty__keyicon__latin_small_letter_i.svg | 39 - .../qwerty__keyicon__latin_small_letter_j.svg | 39 - .../qwerty__keyicon__latin_small_letter_k.svg | 39 - .../qwerty__keyicon__latin_small_letter_l.svg | 39 - .../qwerty__keyicon__latin_small_letter_m.svg | 39 - .../qwerty__keyicon__latin_small_letter_n.svg | 39 - .../qwerty__keyicon__latin_small_letter_o.svg | 39 - .../qwerty__keyicon__latin_small_letter_p.svg | 39 - .../qwerty__keyicon__latin_small_letter_q.svg | 39 - .../qwerty__keyicon__latin_small_letter_r.svg | 39 - .../qwerty__keyicon__latin_small_letter_s.svg | 39 - .../qwerty__keyicon__latin_small_letter_t.svg | 39 - .../qwerty__keyicon__latin_small_letter_u.svg | 39 - .../qwerty__keyicon__latin_small_letter_v.svg | 39 - .../qwerty__keyicon__latin_small_letter_w.svg | 39 - .../qwerty__keyicon__latin_small_letter_x.svg | 39 - .../qwerty__keyicon__latin_small_letter_y.svg | 39 - .../qwerty__keyicon__latin_small_letter_z.svg | 39 - ...keyicon__left_black_lenticular_bracket.svg | 39 - .../qwerty__keyicon__left_corner_bracket.svg | 39 - .../qwerty__keyicon__left_curly_bracket.svg | 39 - .../svg/qwerty__keyicon__left_parenthesis.svg | 39 - .../qwerty__keyicon__left_square_bracket.svg | 39 - .../svg/qwerty__keyicon__less_than_sign.svg | 39 - .../android/svg/qwerty__keyicon__low_line.svg | 39 - .../svg/qwerty__keyicon__minus_sign.svg | 39 - .../qwerty__keyicon__multiplication_sign.svg | 39 - .../svg/qwerty__keyicon__number_sign.svg | 39 - .../qwerty__keyicon__parenthesized_friday.svg | 41 - .../qwerty__keyicon__parenthesized_monday.svg | 41 - ...werty__keyicon__parenthesized_saturday.svg | 41 - .../qwerty__keyicon__parenthesized_sunday.svg | 41 - ...werty__keyicon__parenthesized_thursday.svg | 41 - ...qwerty__keyicon__parenthesized_tuesday.svg | 41 - ...erty__keyicon__parenthesized_wednesday.svg | 41 - .../svg/qwerty__keyicon__percent_sign.svg | 39 - .../svg/qwerty__keyicon__plus_sign.svg | 39 - .../svg/qwerty__keyicon__postal_mark.svg | 39 - .../svg/qwerty__keyicon__question_mark.svg | 39 - .../svg/qwerty__keyicon__quotation_mark.svg | 39 - .../svg/qwerty__keyicon__reference_mark.svg | 39 - .../svg/qwerty__keyicon__reverse_solidus.svg | 39 - ...eyicon__right_black_lenticular_bracket.svg | 39 - .../qwerty__keyicon__right_corner_bracket.svg | 39 - .../qwerty__keyicon__right_curly_bracket.svg | 39 - .../qwerty__keyicon__right_parenthesis.svg | 39 - .../qwerty__keyicon__right_square_bracket.svg | 39 - .../svg/qwerty__keyicon__semicolon.svg | 39 - .../android/svg/qwerty__keyicon__solidus.svg | 39 - .../android/svg/qwerty__keyicon__tilde.svg | 39 - .../svg/qwerty__keyicon__two_dot_leader.svg | 39 - .../svg/qwerty__keyicon__vertical_line.svg | 39 - .../svg/qwerty__keyicon__wave_dash.svg | 39 - .../images/android/svg/qwerty__shift_off.svg | 32 +- .../images/android/svg/qwerty__shift_on.svg | 32 +- .../android/svg/symbol__function__close.svg | 45 +- .../android/svg/symbol__function__delete.svg | 44 - .../android/svg/symbol__major__emoji.svg | 27 +- .../svg/symbol__major__emoji_selected.svg | 27 +- .../android/svg/symbol__major__emoticon.svg | 25 +- .../svg/symbol__major__emoticon_selected.svg | 25 +- ...t__popup.svg => symbol__major__number.svg} | 25 +- .../svg/symbol__major__number_selected.svg | 44 + .../android/svg/symbol__major__symbol.svg | 16 +- .../svg/symbol__major__symbol_selected.svg | 16 +- .../android/svg/symbol__minor__activity.svg | 48 + .../svg/symbol__minor__activity_selected.svg | 48 + .../android/svg/symbol__minor__arrow.svg | 46 + .../svg/symbol__minor__arrow_selected.svg | 46 + .../android/svg/symbol__minor__city.svg | 44 + .../svg/symbol__minor__city_selected.svg | 44 + ...con.svg => symbol__minor__displeasure.svg} | 21 +- .../symbol__minor__displeasure_selected.svg | 44 + .../android/svg/symbol__minor__face.svg | 46 + .../svg/symbol__minor__face_selected.svg | 46 + .../android/svg/symbol__minor__food.svg | 48 + .../svg/symbol__minor__food_selected.svg | 48 + ...elease.svg => symbol__minor__fullhalf.svg} | 29 +- .../svg/symbol__minor__fullhalf_selected.svg | 44 + .../android/svg/symbol__minor__general.svg | 44 + .../svg/symbol__minor__general_selected.svg | 44 + ...release.svg => symbol__minor__history.svg} | 37 +- .../svg/symbol__minor__history_selected.svg | 48 + .../android/svg/symbol__minor__math.svg | 44 + .../svg/symbol__minor__math_selected.svg | 44 + ..._release.svg => symbol__minor__nature.svg} | 34 +- .../svg/symbol__minor__nature_selected.svg | 48 + ...ter.svg => symbol__minor__parenthesis.svg} | 29 +- ...> symbol__minor__parenthesis_selected.svg} | 21 +- .../android/svg/symbol__minor__sadness.svg | 44 + .../svg/symbol__minor__sadness_selected.svg | 44 + .../android/svg/symbol__minor__smile.svg | 44 + .../svg/symbol__minor__smile_selected.svg | 44 + .../android/svg/symbol__minor__surprise.svg | 44 + .../svg/symbol__minor__surprise_selected.svg | 44 + .../android/svg/symbol__minor__sweat.svg | 44 + .../svg/symbol__minor__sweat_selected.svg | 44 + .../android/svg/twelvekeys__alphabet__01.svg | 46 - .../android/svg/twelvekeys__alphabet__03.svg | 41 - .../android/svg/twelvekeys__alphabet__04.svg | 41 - .../android/svg/twelvekeys__alphabet__05.svg | 41 - .../android/svg/twelvekeys__alphabet__06.svg | 41 - .../android/svg/twelvekeys__alphabet__07.svg | 42 - .../android/svg/twelvekeys__alphabet__08.svg | 41 - .../android/svg/twelvekeys__alphabet__09.svg | 42 - .../android/svg/twelvekeys__alphabet__10.svg | 17 +- .../android/svg/twelvekeys__alphabet__11.svg | 45 - .../android/svg/twelvekeys__alphabet__12.svg | 45 - ...vg => twelvekeys__alphabet__popup__10.svg} | 20 +- ...elvekeys__alphabet__support__popup__01.svg | 52 - ...elvekeys__alphabet__support__popup__02.svg | 50 - ...elvekeys__alphabet__support__popup__03.svg | 50 - ...elvekeys__alphabet__support__popup__04.svg | 50 - ...elvekeys__alphabet__support__popup__05.svg | 50 - ...elvekeys__alphabet__support__popup__06.svg | 50 - ...elvekeys__alphabet__support__popup__07.svg | 51 - ...elvekeys__alphabet__support__popup__08.svg | 51 - ...elvekeys__alphabet__support__popup__09.svg | 51 - ...elvekeys__alphabet__support__popup__11.svg | 51 - ...elvekeys__alphabet__support__popup__12.svg | 52 - .../twelvekeys__function__alphabet__icon.svg | 31 +- .../twelvekeys__function__alphabet__popup.svg | 46 - .../twelvekeys__function__delete__icon.svg | 40 - .../svg/twelvekeys__function__kana__icon.svg | 29 +- .../svg/twelvekeys__function__kana__popup.svg | 46 - ...twelvekeys__function__left_arrow__icon.svg | 19 +- ...welvekeys__function__left_arrow__popup.svg | 39 - .../twelvekeys__function__number__icon.svg | 46 - .../twelvekeys__function__number__popup.svg | 46 - ...welvekeys__function__right_arrow__icon.svg | 20 +- ...elvekeys__function__right_arrow__popup.svg | 39 - .../svg/twelvekeys__function__space__icon.svg | 19 +- .../twelvekeys__function__symbol__icon.svg | 37 +- .../svg/twelvekeys__function__undo__icon.svg | 25 +- .../svg/twelvekeys__function__undo__popup.svg | 44 - .../android/svg/twelvekeys__kana__10.svg | 27 +- .../svg/twelvekeys__kana__keyicon__axtu.svg | 40 - .../svg/twelvekeys__kana__keyicon__chi.svg | 39 - .../svg/twelvekeys__kana__keyicon__e.svg | 39 - .../svg/twelvekeys__kana__keyicon__en.svg | 40 - ...ekeys__kana__keyicon__exclamation_mark.svg | 39 - .../svg/twelvekeys__kana__keyicon__extu.svg | 40 - .../svg/twelvekeys__kana__keyicon__fu.svg | 39 - .../svg/twelvekeys__kana__keyicon__ha.svg | 39 - .../svg/twelvekeys__kana__keyicon__he.svg | 39 - .../svg/twelvekeys__kana__keyicon__hi.svg | 39 - .../svg/twelvekeys__kana__keyicon__ho.svg | 39 - .../svg/twelvekeys__kana__keyicon__i.svg | 39 - ...eys__kana__keyicon__ideographic_period.svg | 39 - .../svg/twelvekeys__kana__keyicon__in.svg | 40 - .../svg/twelvekeys__kana__keyicon__ixtu.svg | 40 - .../svg/twelvekeys__kana__keyicon__ka.svg | 39 - .../svg/twelvekeys__kana__keyicon__ke.svg | 39 - .../svg/twelvekeys__kana__keyicon__ki.svg | 39 - .../svg/twelvekeys__kana__keyicon__ko.svg | 39 - .../svg/twelvekeys__kana__keyicon__ku.svg | 39 - .../svg/twelvekeys__kana__keyicon__ma.svg | 39 - .../svg/twelvekeys__kana__keyicon__me.svg | 40 - .../svg/twelvekeys__kana__keyicon__mi.svg | 39 - .../svg/twelvekeys__kana__keyicon__mo.svg | 39 - .../svg/twelvekeys__kana__keyicon__mu.svg | 39 - .../svg/twelvekeys__kana__keyicon__na.svg | 39 - .../svg/twelvekeys__kana__keyicon__ne.svg | 39 - .../svg/twelvekeys__kana__keyicon__ni.svg | 39 - .../svg/twelvekeys__kana__keyicon__nn.svg | 39 - .../svg/twelvekeys__kana__keyicon__no.svg | 39 - .../svg/twelvekeys__kana__keyicon__nu.svg | 39 - .../svg/twelvekeys__kana__keyicon__o.svg | 39 - .../svg/twelvekeys__kana__keyicon__on.svg | 40 - .../svg/twelvekeys__kana__keyicon__oxtu.svg | 40 - ...s__kana__keyicon__prolonged_sound_mark.svg | 39 - ...elvekeys__kana__keyicon__question_mark.svg | 39 - .../svg/twelvekeys__kana__keyicon__ra.svg | 39 - .../svg/twelvekeys__kana__keyicon__re.svg | 39 - .../svg/twelvekeys__kana__keyicon__ri.svg | 39 - .../svg/twelvekeys__kana__keyicon__ro.svg | 39 - .../svg/twelvekeys__kana__keyicon__ru.svg | 39 - .../svg/twelvekeys__kana__keyicon__sa.svg | 39 - .../svg/twelvekeys__kana__keyicon__se.svg | 39 - .../svg/twelvekeys__kana__keyicon__shi.svg | 39 - .../svg/twelvekeys__kana__keyicon__so.svg | 39 - .../svg/twelvekeys__kana__keyicon__su.svg | 39 - .../svg/twelvekeys__kana__keyicon__ta.svg | 39 - .../svg/twelvekeys__kana__keyicon__te.svg | 39 - .../svg/twelvekeys__kana__keyicon__to.svg | 39 - .../svg/twelvekeys__kana__keyicon__tsu.svg | 39 - .../svg/twelvekeys__kana__keyicon__u.svg | 39 - .../svg/twelvekeys__kana__keyicon__un.svg | 40 - .../svg/twelvekeys__kana__keyicon__uxtu.svg | 40 - .../svg/twelvekeys__kana__keyicon__wa.svg | 39 - .../svg/twelvekeys__kana__keyicon__wo.svg | 39 - .../svg/twelvekeys__kana__keyicon__xe.svg | 39 - .../svg/twelvekeys__kana__keyicon__xi.svg | 39 - .../svg/twelvekeys__kana__keyicon__xya.svg | 39 - .../svg/twelvekeys__kana__keyicon__xyo.svg | 40 - .../svg/twelvekeys__kana__keyicon__xyu.svg | 39 - .../svg/twelvekeys__kana__keyicon__ya.svg | 39 - .../svg/twelvekeys__kana__keyicon__yo.svg | 39 - .../svg/twelvekeys__kana__keyicon__yu.svg | 39 - .../svg/twelvekeys__kana__popup__10.svg | 40 +- ...lvekeys__kana__popup__semi_voiced_mark.svg | 18 +- .../svg/twelvekeys__kana__popup__small.svg | 9 +- .../twelvekeys__kana__popup__voiced_mark.svg | 18 +- .../svg/twelvekeys__kana__simple__12.svg | 21 +- .../twelvekeys__kana__support__01_center.svg | 51 - .../twelvekeys__kana__support__01_down.svg | 51 - .../twelvekeys__kana__support__01_left.svg | 51 - .../twelvekeys__kana__support__01_release.svg | 51 - .../twelvekeys__kana__support__01_right.svg | 51 - .../svg/twelvekeys__kana__support__01_up.svg | 51 - .../twelvekeys__kana__support__02_center.svg | 51 - .../twelvekeys__kana__support__02_down.svg | 51 - .../twelvekeys__kana__support__02_left.svg | 51 - .../twelvekeys__kana__support__02_release.svg | 51 - .../twelvekeys__kana__support__02_right.svg | 51 - .../svg/twelvekeys__kana__support__02_up.svg | 51 - .../twelvekeys__kana__support__03_center.svg | 52 - .../twelvekeys__kana__support__03_down.svg | 52 - .../twelvekeys__kana__support__03_left.svg | 52 - .../twelvekeys__kana__support__03_release.svg | 52 - .../twelvekeys__kana__support__03_right.svg | 52 - .../svg/twelvekeys__kana__support__03_up.svg | 52 - .../twelvekeys__kana__support__04_center.svg | 51 - .../twelvekeys__kana__support__04_down.svg | 51 - .../twelvekeys__kana__support__04_left.svg | 51 - .../twelvekeys__kana__support__04_release.svg | 51 - .../twelvekeys__kana__support__04_right.svg | 51 - .../svg/twelvekeys__kana__support__04_up.svg | 51 - .../twelvekeys__kana__support__05_center.svg | 51 - .../twelvekeys__kana__support__05_down.svg | 51 - .../twelvekeys__kana__support__05_left.svg | 51 - .../twelvekeys__kana__support__05_release.svg | 51 - .../twelvekeys__kana__support__05_right.svg | 51 - .../svg/twelvekeys__kana__support__05_up.svg | 51 - .../twelvekeys__kana__support__06_center.svg | 51 - .../twelvekeys__kana__support__06_down.svg | 51 - .../twelvekeys__kana__support__06_left.svg | 51 - .../twelvekeys__kana__support__06_release.svg | 51 - .../twelvekeys__kana__support__06_right.svg | 51 - .../svg/twelvekeys__kana__support__06_up.svg | 51 - .../twelvekeys__kana__support__07_center.svg | 52 - .../twelvekeys__kana__support__07_down.svg | 51 - .../twelvekeys__kana__support__07_left.svg | 52 - .../twelvekeys__kana__support__07_release.svg | 52 - .../twelvekeys__kana__support__07_right.svg | 52 - .../svg/twelvekeys__kana__support__07_up.svg | 52 - .../twelvekeys__kana__support__08_center.svg | 49 - .../twelvekeys__kana__support__08_down.svg | 49 - .../twelvekeys__kana__support__08_release.svg | 49 - .../svg/twelvekeys__kana__support__08_up.svg | 49 - .../twelvekeys__kana__support__09_center.svg | 51 - .../twelvekeys__kana__support__09_down.svg | 51 - .../twelvekeys__kana__support__09_left.svg | 51 - .../twelvekeys__kana__support__09_release.svg | 51 - .../twelvekeys__kana__support__09_right.svg | 51 - .../svg/twelvekeys__kana__support__09_up.svg | 51 - .../twelvekeys__kana__support__11_center.svg | 50 - .../twelvekeys__kana__support__11_left.svg | 50 - .../twelvekeys__kana__support__11_release.svg | 50 - .../twelvekeys__kana__support__11_right.svg | 50 - .../svg/twelvekeys__kana__support__11_up.svg | 50 - ...=> twelvekeys__kana__support__12_down.svg} | 34 +- .../twelvekeys__kana__support__12_left.svg | 51 - .../twelvekeys__kana__support__12_release.svg | 53 - .../twelvekeys__kana__support__12_right.svg | 53 - .../svg/twelvekeys__kana__support__12_up.svg | 53 - .../twelvekeys__kana__support__popup__01.svg | 53 - .../twelvekeys__kana__support__popup__02.svg | 52 - .../twelvekeys__kana__support__popup__03.svg | 54 - .../twelvekeys__kana__support__popup__04.svg | 52 - .../twelvekeys__kana__support__popup__05.svg | 52 - .../twelvekeys__kana__support__popup__06.svg | 52 - .../twelvekeys__kana__support__popup__07.svg | 52 - .../twelvekeys__kana__support__popup__08.svg | 50 - .../twelvekeys__kana__support__popup__09.svg | 52 - .../twelvekeys__kana__support__popup__11.svg | 51 - .../twelvekeys__kana__support__popup__12.svg | 51 - .../android/svg/twelvekeys__number__04.svg | 39 - .../android/svg/twelvekeys__number__05.svg | 39 - .../android/svg/twelvekeys__number__06.svg | 39 - .../android/svg/twelvekeys__number__07.svg | 39 - .../android/svg/twelvekeys__number__08.svg | 39 - .../android/svg/twelvekeys__number__09.svg | 39 - .../android/svg/twelvekeys__number__10.svg | 45 - .../android/svg/twelvekeys__number__11.svg | 39 - .../android/svg/twelvekeys__number__12.svg | 45 - ...twelvekeys__number__support__popup__10.svg | 51 - ...twelvekeys__number__support__popup__12.svg | 51 - .../godan__kana__support__12__template.svg} | 25 +- .../godan__kana__support__template.svg} | 25 +- .../qwerty__keyicon__template.svg} | 17 +- .../template/qwerty__popup__template.svg | 43 + .../support__popup__template.svg} | 30 +- src/data/images/android/template/transform.py | 718 ++++++++++ ...twelvekeys__alphabet__popup__template.svg} | 15 +- ...elvekeys__alphabet__support__template.svg} | 15 +- .../twelvekeys__alphabet__template.svg} | 8 +- .../twelvekeys__kana__keyicon__template.svg} | 8 +- ...welvekeys__kana__support__12__template.svg | 46 + .../twelvekeys__kana__support__template.svg | 46 + ...welvekeys__number__function__template.svg} | 10 +- .../twelvekeys__number__template.svg} | 8 +- .../twelvekeys__popup__template.svg} | 10 +- src/data/preedit/12keys-halfwidthascii.tsv | 6 +- src/data/preedit/12keys-number.tsv | 19 - src/data/preedit/flick-halfwidthascii.tsv | 11 +- src/data/preedit/flick-number.tsv | 9 - src/data/preedit/notouch-hiragana.tsv | 128 ++ .../preedit/qwerty_mobile-hiragana-number.tsv | 39 - .../preedit/toggle_flick-halfwidthascii.tsv | 17 +- src/data/preedit/toggle_flick-hiragana.tsv | 12 +- src/data/preedit/toggle_flick-number.tsv | 25 - .../session/scenario/b12751061_scenario.txt | 2 +- .../scenario/mobile_t13n_candidates.txt | 93 +- .../twelvekeys_switch_inputmode_scenario.txt | 16 +- src/docker/ubuntu12.04/Dockerfile | 2 +- src/docker/ubuntu14.04/Dockerfile | 2 +- src/gyp/tests.gyp | 5 +- src/mozc_version_template.txt | 2 +- src/session/commands.proto | 25 +- src/session/session_handler_scenario_test.cc | 1 + src/session/session_test.cc | 26 +- 804 files changed, 26507 insertions(+), 29570 deletions(-) create mode 100644 src/android/android_resources.gypi create mode 100644 src/android/gen_subset_font.py rename src/android/{static_resources/resources_oss => resources}/AndroidManifest.xml (99%) rename src/android/{static_resources/resources_oss => resources}/ant.properties (100%) rename src/android/{static_resources/resources_oss => resources}/build.xml (100%) create mode 100644 src/android/resources/proguard-project.txt rename src/android/{static_resources/resources_oss => resources}/project.properties (99%) create mode 100644 src/android/resources/resources.gyp create mode 100644 src/android/resources/src/DONT_REMOVE_THIS_DIRECTORY create mode 100644 src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java delete mode 100644 src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java delete mode 100644 src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java rename src/android/{tests/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParserTest.java => src/com/google/android/inputmethod/japanese/LauncherActivity.java} (55%) rename src/android/src/com/google/android/inputmethod/japanese/{JapaneseKeyboardView.java => LauncherIconVisibilityInitializer.java} (60%) create mode 100644 src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java rename src/android/src/com/google/android/inputmethod/japanese/{JapaneseKeyboardFactory.java => keyboard/KeyboardFactory.java} (82%) create mode 100644 src/android/src/com/google/android/inputmethod/japanese/ui/FloatingCandidateLayoutRenderer.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/ui/FloatingModeIndicator.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/ui/InputFrameFoldButtonView.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/ui/PopUpLayouter.java delete mode 100644 src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryActionBarHelperFactory.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/util/CandidateDescriptionUtil.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/util/LauncherIconManagerFactory.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/util/ParserUtil.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/DummyDrawable.java delete mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/LightIconDrawable.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/MozcImageButton.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/MozcImageCapableView.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/MozcImageView.java delete mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/PopUpFrameWindowDrawable.java rename src/android/src/com/google/android/inputmethod/japanese/view/{VerticalInnerDropShadowDrawable.java => QwertySpaceKeyDrawable.java} (50%) create mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/Skin.java create mode 100644 src/android/src/com/google/android/inputmethod/japanese/view/SkinParser.java rename src/android/static_resources/{resources_oss/res => application_icon/oss_icon}/drawable-hdpi/application_icon.png (100%) create mode 100644 src/android/static_resources/application_icon/oss_icon/drawable-mdpi/application_icon.png rename src/android/static_resources/{resources_oss/res => application_icon/oss_icon}/drawable-xhdpi/application_icon.png (100%) create mode 100644 src/android/static_resources/application_icon/oss_icon/drawable-xxhdpi/application_icon.png create mode 100644 src/android/static_resources/application_icon/oss_icon/drawable-xxxhdpi/application_icon.png rename src/android/{tests/res/values/dummy.xml => static_resources/launcher_icon_resources/launcher_icon_preinstall_bools.xml} (92%) rename src/android/static_resources/{resources_oss/res/drawable/dropshadow_right.xml => launcher_icon_resources/launcher_icon_standard_bools.xml} (87%) create mode 100644 src/android/static_resources/resources_oss/res/drawable-hdpi/window__background_dark.png create mode 100644 src/android/static_resources/resources_oss/res/layout/button_frame.xml delete mode 100644 src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_action_bar_view.xml delete mode 100644 src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_empty_scrollview.xml create mode 100644 src/android/static_resources/resources_oss/res/values-h380dp-land/dimens.xml rename src/android/static_resources/resources_oss/res/{drawable/dropshadow_top.xml => values-h500dp-land/config.xml} (87%) create mode 100644 src/android/static_resources/resources_oss/res/values-h500dp-land/dimens.xml rename src/android/static_resources/resources_oss/res/{drawable/dropshadow_left.xml => values-h570dp-land/config.xml} (87%) create mode 100644 src/android/static_resources/resources_oss/res/values-h570dp-land/dimens.xml create mode 100644 src/android/static_resources/resources_oss/res/values-h570dp-land/raws.xml create mode 100644 src/android/static_resources/resources_oss/res/values-h650dp-port/dimens.xml rename src/android/static_resources/resources_oss/res/{values/styles.xml => values-h720dp-land/dimens.xml} (62%) create mode 100644 src/android/static_resources/resources_oss/res/values-h800dp-port/config.xml create mode 100644 src/android/static_resources/resources_oss/res/values-h800dp-port/dimens.xml create mode 100644 src/android/static_resources/resources_oss/res/values-h800dp-port/raws.xml create mode 100644 src/android/static_resources/resources_oss/res/values-h900dp-port/dimens.xml delete mode 100644 src/android/static_resources/resources_oss/res/values-land/strings.xml create mode 100644 src/android/static_resources/resources_oss/res/values-v16/bools.xml create mode 100644 src/android/static_resources/resources_oss/res/values-v21/strings.xml create mode 100644 src/android/static_resources/resources_oss/res/values-v21/themes.xml rename src/android/static_resources/resources_oss/res/{values-land/config.xml => values/arrays_skin.xml} (67%) create mode 100644 src/android/static_resources/resources_oss/res/values/raws.xml create mode 100644 src/android/static_resources/resources_oss/res/values/themes.xml create mode 100644 src/android/static_resources/resources_oss/res/values/untranslatable_application.xml create mode 100644 src/android/static_resources/resources_oss/res/values/untranslatable_strings.xml create mode 100644 src/android/static_resources/resources_oss/res/xml/kbd_123.xml delete mode 100644 src/android/static_resources/resources_oss/res/xml/kbd_12keys_123.xml delete mode 100644 src/android/static_resources/resources_oss/res/xml/kbd_12keys_qwerty_abc.xml delete mode 100644 src/android/static_resources/resources_oss/res/xml/kbd_qwerty_kana_123.xml rename src/android/static_resources/resources_oss/res/xml/{kbd_12keys_flick_123.xml => kbd_symbol_123.xml} (51%) create mode 100644 src/android/static_resources/resources_oss/res/xml/keyboard_layouts.xml create mode 100644 src/android/static_resources/resources_oss/res/xml/skin_blue_darkgray.xml create mode 100644 src/android/static_resources/resources_oss/res/xml/skin_blue_lightgray.xml create mode 100644 src/android/static_resources/resources_oss/res/xml/skin_material_design_dark.xml create mode 100644 src/android/static_resources/resources_oss/res/xml/skin_material_design_light.xml create mode 100644 src/android/static_resources/resources_oss/res/xml/skin_orange_lightgray.xml create mode 100644 src/android/tests/res/values-land/dimens.xml create mode 100644 src/android/tests/res/values/dimens.xml create mode 100644 src/android/tests/res/xml/skinparser_correct_drawable_test_1.xml create mode 100644 src/android/tests/res/xml/skinparser_incorrect_drawable_test_1.xml create mode 100644 src/android/tests/res/xml/skinparser_test_1.xml create mode 100644 src/android/tests/res/xml/skinparser_test_2.xml create mode 100644 src/android/tests/res/xml/skinparser_test_3.xml create mode 100644 src/android/tests/res/xml/skinparser_test_4.xml create mode 100644 src/android/tests/res/xml/skinparser_test_5.xml create mode 100644 src/android/tests/res/xml/skinparser_test_6.xml create mode 100644 src/android/tests/res/xml/skinparser_test_7.xml create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/CandidateViewManagerTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/FloatingCandidateViewTest.java delete mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/JapaneseKeyboardViewTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/NarrowFrameViewTest.java rename src/android/tests/src/com/google/android/inputmethod/japanese/{JapaneseKeyboardFactoryTest.java => keyboard/KeyboardFactoryTest.java} (90%) rename src/android/tests/src/com/google/android/inputmethod/japanese/{JapaneseKeyboardTest.java => keyboard/KeyboardTest.java} (80%) create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/testing/MockContext.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/testing/MockDrawable.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/testing/MockInputStream.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/testing/MockPackageManager.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/testing/MockResourcesWithDisplayMetrics.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/testing/MockWindow.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/ui/FloatingCandidateLayoutRendererTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/ui/FloatingModeIndicatorTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/ui/PopUpLayouterTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/util/CandidateDescriptionUtilTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/util/LauncherIconManagerFactoryTest.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/vectorgraphic/BitmapSaver.java create mode 100644 src/android/tests/src/com/google/android/inputmethod/japanese/view/SkinParserTest.java create mode 100644 src/data/images/android/svg/floating_mode_indicator__alphabet_normal.svg rename src/data/images/android/svg/{qwerty__function__text_kana__icon.svg => floating_mode_indicator__kana_normal.svg} (69%) create mode 100644 src/data/images/android/svg/function__action_done__icon.svg rename src/data/images/android/svg/{godan__function__kana__icon.svg => function__action_go__icon.svg} (68%) create mode 100644 src/data/images/android/svg/function__action_next__icon.svg create mode 100644 src/data/images/android/svg/function__action_previous__icon.svg rename src/data/images/android/svg/{magnifier.svg => function__action_search__icon.svg} (64%) create mode 100644 src/data/images/android/svg/function__action_send__icon.svg create mode 100644 src/data/images/android/svg/function__enter__icon.svg create mode 100644 src/data/images/android/svg/function__symbol__popup.svg delete mode 100644 src/data/images/android/svg/globe.svg create mode 100644 src/data/images/android/svg/globe_keyicon.svg create mode 100644 src/data/images/android/svg/globe_qwerty_function.svg delete mode 100644 src/data/images/android/svg/godan__function__undo__icon.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__01_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__01_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__02_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__02_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__02_right.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__02_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__03_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__03_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__03_left.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__03_right.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__03_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__04_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__04_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__05_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__05_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__05_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__05_right.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__05_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__06_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__06_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__06_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__06_right.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__06_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__07_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__07_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__07_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__08_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__08_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__08_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__08_right.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__08_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__09_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__09_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__09_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__09_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__10_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__11_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__11_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__11_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__12_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__12_down.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__12_left.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__12_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__12_right.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__12_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__13_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__15_center.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__15_release.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__15_up.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__01.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__02.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__03.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__04.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__05.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__06.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__07.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__08.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__09.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__10.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__11.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__12.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__13.svg delete mode 100644 src/data/images/android/svg/godan__kana__support__popup__15.svg rename src/data/images/android/svg/{godan__function__delete__icon.svg => keyboard_fold_tab_background_default.svg} (62%) create mode 100644 src/data/images/android/svg/keyboard_fold_tab_background_scrolled.svg create mode 100644 src/data/images/android/svg/microphone.svg rename src/data/images/android/svg/{godan__function__right_arrow__icon.svg => qwerty__function__alt__alted_icon.svg} (70%) rename src/data/images/android/svg/{qwerty__keyicon__asterisk.svg => qwerty__function__alt__base_icon.svg} (85%) delete mode 100644 src/data/images/android/svg/qwerty__function__cursor__icon.svg delete mode 100644 src/data/images/android/svg/qwerty__function__delete__icon.svg delete mode 100644 src/data/images/android/svg/qwerty__function__delete__popup.svg delete mode 100644 src/data/images/android/svg/qwerty__function__enter__icon.svg delete mode 100644 src/data/images/android/svg/qwerty__function__enter__popup.svg rename src/data/images/android/svg/{qwerty__keyicon__apostrophe.svg => qwerty__function__exclamation_mark.svg} (78%) delete mode 100644 src/data/images/android/svg/qwerty__function__kana__popup.svg create mode 100644 src/data/images/android/svg/qwerty__function__left_arrow__icon.svg delete mode 100644 src/data/images/android/svg/qwerty__function__number_alphabet__popup.svg delete mode 100644 src/data/images/android/svg/qwerty__function__number_kana__icon.svg delete mode 100644 src/data/images/android/svg/qwerty__function__number_kana__popup.svg rename src/data/images/android/svg/{qwerty__keyicon__black_heart_suit.svg => qwerty__function__question_mark.svg} (78%) rename src/data/images/android/svg/{qwerty__function__alt__icon.svg => qwerty__function__right_arrow__icon.svg} (73%) delete mode 100644 src/data/images/android/svg/qwerty__function__space__popup.svg delete mode 100644 src/data/images/android/svg/qwerty__function__symbol__popup.svg delete mode 100644 src/data/images/android/svg/qwerty__function__text_alphabet__popup.svg delete mode 100644 src/data/images/android/svg/qwerty__function__text_kana__popup.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__black_star.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__circumflex_accent.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__cjk_day.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__cjk_hour.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__cjk_minute.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__cjk_month.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__colon.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__comma.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__commercial_at.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_eight.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_five.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_four.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_nine.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_one.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_seven.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_six.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_three.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_two.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__digit_zero.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__division_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__dollar_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__eighth_note.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__equals_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__exclamation_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__full_stop.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_colon.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_equals_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_exclamation_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_left_curly_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_left_parenthesis.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_plus_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_question_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_right_curly_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_right_parenthesis.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_semicolon.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__fullwidth_solidus.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__grave_accent.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__greater_than_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__horizontal_ellipsis.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__hyphen_minus.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__ideographic_comma.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__ideographic_full_stop.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__katakana_middle_dot.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_a.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_b.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_c.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_d.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_e.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_f.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_g.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_h.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_i.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_j.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_k.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_l.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_m.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_n.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_o.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_p.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_q.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_r.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_s.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_t.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_u.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_v.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_w.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_x.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_y.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_capital_letter_z.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_a.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_b.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_c.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_d.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_e.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_f.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_g.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_h.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_i.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_j.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_k.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_l.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_m.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_n.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_o.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_p.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_q.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_r.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_s.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_t.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_u.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_v.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_w.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_x.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_y.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__latin_small_letter_z.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__left_black_lenticular_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__left_corner_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__left_curly_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__left_parenthesis.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__left_square_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__less_than_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__low_line.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__minus_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__multiplication_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__number_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_friday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_monday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_saturday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_sunday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_thursday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_tuesday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__parenthesized_wednesday.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__percent_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__plus_sign.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__postal_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__question_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__quotation_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__reference_mark.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__reverse_solidus.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__right_black_lenticular_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__right_corner_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__right_curly_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__right_parenthesis.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__right_square_bracket.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__semicolon.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__solidus.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__tilde.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__two_dot_leader.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__vertical_line.svg delete mode 100644 src/data/images/android/svg/qwerty__keyicon__wave_dash.svg delete mode 100644 src/data/images/android/svg/symbol__function__delete.svg rename src/data/images/android/svg/{qwerty__function__alphabet__popup.svg => symbol__major__number.svg} (68%) create mode 100644 src/data/images/android/svg/symbol__major__number_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__activity.svg create mode 100644 src/data/images/android/svg/symbol__minor__activity_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__arrow.svg create mode 100644 src/data/images/android/svg/symbol__minor__arrow_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__city.svg create mode 100644 src/data/images/android/svg/symbol__minor__city_selected.svg rename src/data/images/android/svg/{godan__function__space__icon.svg => symbol__minor__displeasure.svg} (68%) create mode 100644 src/data/images/android/svg/symbol__minor__displeasure_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__face.svg create mode 100644 src/data/images/android/svg/symbol__minor__face_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__food.svg create mode 100644 src/data/images/android/svg/symbol__minor__food_selected.svg rename src/data/images/android/svg/{godan__kana__support__13_release.svg => symbol__minor__fullhalf.svg} (68%) create mode 100644 src/data/images/android/svg/symbol__minor__fullhalf_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__general.svg create mode 100644 src/data/images/android/svg/symbol__minor__general_selected.svg rename src/data/images/android/svg/{godan__kana__support__03_release.svg => symbol__minor__history.svg} (59%) create mode 100644 src/data/images/android/svg/symbol__minor__history_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__math.svg create mode 100644 src/data/images/android/svg/symbol__minor__math_selected.svg rename src/data/images/android/svg/{godan__kana__support__01_release.svg => symbol__minor__nature.svg} (67%) create mode 100644 src/data/images/android/svg/symbol__minor__nature_selected.svg rename src/data/images/android/svg/{godan__kana__support__10_center.svg => symbol__minor__parenthesis.svg} (68%) rename src/data/images/android/svg/{godan__function__left_arrow__icon.svg => symbol__minor__parenthesis_selected.svg} (67%) create mode 100644 src/data/images/android/svg/symbol__minor__sadness.svg create mode 100644 src/data/images/android/svg/symbol__minor__sadness_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__smile.svg create mode 100644 src/data/images/android/svg/symbol__minor__smile_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__surprise.svg create mode 100644 src/data/images/android/svg/symbol__minor__surprise_selected.svg create mode 100644 src/data/images/android/svg/symbol__minor__sweat.svg create mode 100644 src/data/images/android/svg/symbol__minor__sweat_selected.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__01.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__03.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__04.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__05.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__06.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__07.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__08.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__09.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__11.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__12.svg rename src/data/images/android/svg/{godan__function__enter__icon.svg => twelvekeys__alphabet__popup__10.svg} (69%) delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__01.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__02.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__03.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__04.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__05.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__06.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__07.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__08.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__09.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__11.svg delete mode 100644 src/data/images/android/svg/twelvekeys__alphabet__support__popup__12.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__alphabet__popup.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__delete__icon.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__kana__popup.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__left_arrow__popup.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__number__icon.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__number__popup.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__right_arrow__popup.svg delete mode 100644 src/data/images/android/svg/twelvekeys__function__undo__popup.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__axtu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__chi.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__e.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__en.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__exclamation_mark.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__extu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__fu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ha.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__he.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__hi.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ho.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__i.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ideographic_period.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__in.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ixtu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ka.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ke.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ki.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ko.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ku.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ma.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__me.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__mi.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__mo.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__mu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__na.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ne.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ni.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__nn.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__no.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__nu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__o.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__on.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__oxtu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__prolonged_sound_mark.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__question_mark.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ra.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__re.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ri.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ro.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ru.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__sa.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__se.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__shi.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__so.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__su.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ta.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__te.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__to.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__tsu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__u.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__un.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__uxtu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__wa.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__wo.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__xe.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__xi.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__xya.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__xyo.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__xyu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__ya.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__yo.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__keyicon__yu.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__01_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__01_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__01_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__01_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__01_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__01_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__02_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__02_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__02_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__02_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__02_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__02_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__03_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__03_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__03_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__03_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__03_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__03_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__04_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__04_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__04_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__04_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__04_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__04_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__05_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__05_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__05_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__05_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__05_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__05_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__06_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__06_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__06_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__06_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__06_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__06_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__07_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__07_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__07_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__07_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__07_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__07_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__08_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__08_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__08_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__08_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__09_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__09_down.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__09_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__09_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__09_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__09_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__11_center.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__11_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__11_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__11_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__11_up.svg rename src/data/images/android/svg/{twelvekeys__kana__support__12_center.svg => twelvekeys__kana__support__12_down.svg} (64%) delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__12_left.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__12_release.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__12_right.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__12_up.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__01.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__02.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__03.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__04.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__05.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__06.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__07.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__08.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__09.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__11.svg delete mode 100644 src/data/images/android/svg/twelvekeys__kana__support__popup__12.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__04.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__05.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__06.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__07.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__08.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__09.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__10.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__11.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__12.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__support__popup__10.svg delete mode 100644 src/data/images/android/svg/twelvekeys__number__support__popup__12.svg rename src/data/images/android/{svg/godan__kana__support__04_release.svg => template/godan__kana__support__12__template.svg} (57%) rename src/data/images/android/{svg/godan__kana__support__02_release.svg => template/godan__kana__support__template.svg} (64%) rename src/data/images/android/{svg/twelvekeys__function__enter__icon.svg => template/qwerty__keyicon__template.svg} (72%) create mode 100644 src/data/images/android/template/qwerty__popup__template.svg rename src/data/images/android/{svg/godan__function__symbol__icon.svg => template/support__popup__template.svg} (61%) create mode 100644 src/data/images/android/template/transform.py rename src/data/images/android/{svg/qwerty__keyicon__ampersand.svg => template/twelvekeys__alphabet__popup__template.svg} (78%) rename src/data/images/android/{svg/twelvekeys__alphabet__02.svg => template/twelvekeys__alphabet__support__template.svg} (74%) rename src/data/images/android/{svg/twelvekeys__number__01.svg => template/twelvekeys__alphabet__template.svg} (82%) rename src/data/images/android/{svg/twelvekeys__kana__keyicon__a.svg => template/twelvekeys__kana__keyicon__template.svg} (83%) create mode 100644 src/data/images/android/template/twelvekeys__kana__support__12__template.svg create mode 100644 src/data/images/android/template/twelvekeys__kana__support__template.svg rename src/data/images/android/{svg/twelvekeys__number__03.svg => template/twelvekeys__number__function__template.svg} (81%) rename src/data/images/android/{svg/twelvekeys__number__02.svg => template/twelvekeys__number__template.svg} (82%) rename src/data/images/android/{svg/twelvekeys__kana__keyicon__an.svg => template/twelvekeys__popup__template.svg} (81%) delete mode 100644 src/data/preedit/12keys-number.tsv delete mode 100644 src/data/preedit/flick-number.tsv create mode 100644 src/data/preedit/notouch-hiragana.tsv delete mode 100644 src/data/preedit/qwerty_mobile-hiragana-number.tsv delete mode 100644 src/data/preedit/toggle_flick-number.tsv diff --git a/src/DEPS b/src/DEPS index d58edc484..03c4ff483 100644 --- a/src/DEPS +++ b/src/DEPS @@ -29,6 +29,7 @@ vars = { "breakpad_revision": "1391", + "fonttools_revision": "5ba7d98a4153fad57258fca23b0bcb238717aec3", "gtest_revision": "700", "gmock_revision": "501", "gyp_revision": "2012", @@ -82,6 +83,9 @@ deps_os = { File("http://findbugs.googlecode.com/" + "svn/repos/release-repository/com/google/code/findbugs/jsr305/" + Var("jsr305_version") + "/jsr305-" + Var("jsr305_version") + ".jar"), + "src/third_party/fontTools": + "https://github.com/googlei18n/fonttools.git@" + + Var("fonttools_revision"), "src/third_party/zlib/v1_2_8": "https://src.chromium.org/chrome/trunk/src/third_party/zlib@" + Var("zlib_revision"), diff --git a/src/android/AndroidManifest_template.xml b/src/android/AndroidManifest_template.xml index 988b82308..5d3a8459f 100644 --- a/src/android/AndroidManifest_template.xml +++ b/src/android/AndroidManifest_template.xml @@ -33,15 +33,21 @@ package="@ANDROID_APPLICATION_ID@" android:versionCode="@ANDROID_VERSION_CODE@" android:versionName="@MAJOR@.@MINOR@.@BUILD@.@REVISION@-@ANDROID_ARCH@"> - + + + + - + @@ -51,18 +57,32 @@ - - + + + + + + + + + + + + + android:launchMode="singleTask" + android:theme="@style/AppThemeSelectorSettings"/> + android:launchMode="singleTop" + android:theme="@style/AppThemeSelectorSettings"/> + android:configChanges="orientation|screenSize" + android:theme="@style/AppThemeSelectorSettings"> + @@ -111,11 +131,20 @@ + + + + + + + + diff --git a/src/android/android.gyp b/src/android/android.gyp index 9943cf830..147f2a39a 100644 --- a/src/android/android.gyp +++ b/src/android/android.gyp @@ -100,6 +100,7 @@ 'type': 'none', 'dependencies': [ 'protobuf/protobuf.gyp:protobuf_java', + 'resources/resources.gyp:resources', 'sdk_apk_dependencies', 'userfeedback/userfeedback.gyp:userfeedback', ], @@ -153,9 +154,10 @@ 'android_manifest', 'assets', 'mozc', - 'gen_mozc_drawable', 'guava_library', 'userfeedback/userfeedback.gyp:userfeedback_project', + 'subset_font', + 'resources/resources.gyp:resources_project', 'support_libraries', ], }, @@ -219,7 +221,6 @@ 'files': [ # Copies the copyright and credit info. '../data/installer/credits_en.html', - '../data/installer/credits_ja.html', ], }], }, @@ -407,7 +408,7 @@ 'outputs': ['dummy_touch_stat_data'], 'action': [ 'python', 'gen_touch_event_stats.py', - '--output_dir', 'assets', + '--output_dir', '<(sdk_asset_dir)', '--stats_data', '../data/typing/touch_event_stats.csv', '--collected_keyboards', 'collected_keyboards.csv', ], @@ -528,22 +529,34 @@ ], }, { - 'target_name': 'gen_mozc_drawable', + 'target_name': 'subset_font', 'type': 'none', + 'dependencies': [ + # TODO(komatsu): Is it better to move android_base.gyp? + 'resources/resources.gyp:copy_asis_svg', + 'resources/resources.gyp:transform_template_svg', + ], + 'variables': { + 'input_font': '<(font_dir)/Noto-Roboto2-Regular.otf', + 'fonttools_path': '<(third_party_dir)/fontTools/Lib/fontTools', + }, 'actions': [ { - 'action_name': 'generate_pic_files', + 'action_name': 'make_subset_font', 'inputs': [ - '<(dummy_input_file)', - 'gen_mozc_drawable.py', + '<(input_font)', + 'gen_subset_font.py', ], 'outputs': [ - 'dummy_gen_mozc_drawable_output', + '<(sdk_asset_dir)/subset_font.otf', ], 'action': [ - 'python', 'gen_mozc_drawable.py', - '--svg_dir=../data/images/android/svg', - '--output_dir=<(resources_project_path)/res/raw', + 'python', + 'gen_subset_font.py', + '--svg_paths=<(shared_intermediate_mozc_dir)/data/images/android/svg/transformed.zip,<(shared_intermediate_mozc_dir)/data/images/android/svg/asis.zip', + '--input_font', '<(input_font)', + '--output_font', '<@(_outputs)', + '--fonttools_path', '<(fonttools_path)', ], }, ], diff --git a/src/android/android_env.gypi b/src/android/android_env.gypi index c92a1ba51..579b53f48 100644 --- a/src/android/android_env.gypi +++ b/src/android/android_env.gypi @@ -78,7 +78,7 @@ 'native_test_small_targets': [ 'oss_data_manager_test', ], - 'resources_project_path': 'static_resources/resources_oss', + 'font_dir': '<(third_party_dir)/noto_font', }, }], ['android_arch=="arm" and android_compiler=="gcc"', { diff --git a/src/android/android_resources.gypi b/src/android/android_resources.gypi new file mode 100644 index 000000000..853b35db9 --- /dev/null +++ b/src/android/android_resources.gypi @@ -0,0 +1,60 @@ +# Copyright 2010-2014, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +{ + 'variables': { + 'android_translations': [ + 'B', value & 0xFF)) @@ -187,15 +212,21 @@ def WriteFloat(self, value): # so we can compress the data by using fixed-precision values. self.output.write(struct.pack('>f', value)) + def WriteString(self, value): + utf8_string = value.encode('utf-8') + self.WriteInt16(len(utf8_string)) + self.output.write(utf8_string) + def __enter__(self): self.output.__enter__() - def __exit__(self): - self.output.__exit__() + def __exit__(self, exec_type, exec_value, traceback): + self.output.__exit__(exec_type, exec_value, traceback) class MozcDrawableConverter(object): """Converter from .svg file to .pic file.""" + def __init__(self): pass @@ -209,12 +240,22 @@ def _ParseColor(self, color): if not m: return None - c = int(m.group(1), 16) - if c < 0 or 0x1000000 <= c: + c_str = m.group(1) + if 3 <= len(c_str) <= 4: + expanded_c_str = '' + for ch in c_str: + expanded_c_str = expanded_c_str + ch + ch + c_str = expanded_c_str + if len(c_str) == 6: + c_str = 'FF' + c_str + if len(c_str) != 8: + logging.critical('Invalid color format.') + sys.exit(1) + c = int(c_str, 16) + if c < 0 or 0x100000000 <= c: logging.critical('Out of color range: %s', color) sys.exit(1) - # Set alpha. - return c | 0xFF000000 + return c def _ParseShader(self, color, shader_map): """Parses shader attribute and returns a shader name from the given map.""" @@ -278,77 +319,104 @@ def _ParseShaderMap(self, node): y1 = float(node.get('y1')) x2 = float(node.get('x2')) y2 = float(node.get('y2')) - gradientTransform = node.get('gradientTransform') - if gradientTransform: - m = MATRIX_PATTERN.match(gradientTransform) - (m11, m21, m12, m22, m13, m23) = self._ParseFloatList(m.group(1)) + gradient_transform = node.get('gradientTransform') + if gradient_transform: + m = MATRIX_PATTERN.match(gradient_transform) + (m11, m21, m12, m22, m13, m23) = tuple(self._ParseFloatList(m.group(1))) (x1, y1) = (m11 * x1 + m12 * y1 + m13, m21 * x1 + m22 * y1 + m23) (x2, y2) = (m11 * x2 + m12 * y2 + m13, m21 * x2 + m22 * y2 + m23) color_list = self._ParseStopList(node) - return { element_id: ('linear', x1, y1, x2, y2, color_list) } + return {element_id: ('linear', x1, y1, x2, y2, color_list)} if node.tag == '{http://www.w3.org/2000/svg}radialGradient': element_id = node.get('id') cx = float(node.get('cx')) cy = float(node.get('cy')) r = float(node.get('r')) - gradientTransform = node.get('gradientTransform') - if gradientTransform: - m = MATRIX_PATTERN.match(gradientTransform) + gradient_transform = node.get('gradientTransform') + if gradient_transform: + m = MATRIX_PATTERN.match(gradient_transform) matrix = self._ParseFloatList(m.group(1)) else: matrix = None color_list = self._ParseStopList(node) - return { element_id: ('radial', cx, cy, r, matrix, color_list) } + return {element_id: ('radial', cx, cy, r, matrix, color_list)} return {} def _ParseStyle(self, node, has_shadow, shader_map): """Parses style attribute of the given node.""" - result = {} + common_map = {} + # Default fill color is black (SVG's spec) + fill_map = {'style': 'fill', 'color': 0xFF000000, 'shadow': has_shadow} + # Default stroke color is none (SVG's spec) + stroke_map = {'style': 'stroke', 'shadow': has_shadow} + + # Special warning for font-size. + # Inkscape often unexpectedly converts from sytle to font-size attribute. + if node.get('font-size', ''): + logging.warning('font-size attribute is not supported.') + for attr in node.get('style', '').split(';'): attr = attr.strip() if not attr: continue command, arg = attr.split(':') if command == 'fill' or command == 'stroke': + paint_map = fill_map if command == 'fill' else stroke_map + if arg == 'none': + paint_map.pop('color', None) + paint_map.pop('shader', None) + continue + shader = self._ParseShader(arg, shader_map) color = self._ParseColor(arg) - if shader is None and color is None: if arg != 'none': - logging.error('Unknown pattern: %s', arg) + logging.critical('Unknown pattern: %s', arg) + sys.exit(1) continue - paint_map = {} paint_map['style'] = command if shader is not None: paint_map['shader'] = shader if color is not None: paint_map['color'] = color - paint_map['shadow'] = has_shadow - - result[command] = paint_map continue if command == 'stroke-width': - paint_map = result['stroke'] - paint_map['stroke-width'] = float(arg) + stroke_map['stroke-width'] = float(arg) continue if command == 'stroke-linecap': - paint_map = result['stroke'] - paint_map['stroke-linecap'] = arg + stroke_map['stroke-linecap'] = arg continue if command == 'stroke-linejoin': - paint_map = result['stroke'] - paint_map['stroke-linejoin'] = arg + stroke_map['stroke-linejoin'] = arg continue - return sorted(result.values(), key=lambda e: e['style']) + # font relating attributes are common to all commands. + if command == 'font-size': + common_map['font-size'] = self._ParsePixel(arg) + if command == 'text-anchor': + common_map['text-anchor'] = arg + if command == 'dominant-baseline': + common_map['dominant-baseline'] = arg + if command == 'font-weight': + common_map['font-weight'] = arg + + # 'fill' comes first in order to draw 'fill' first (SVG specification). + result = [] + if 'color' in fill_map or 'shader' in fill_map: + fill_map.update(common_map) + result.append(fill_map) + if 'color' in stroke_map or 'shader' in stroke_map: + stroke_map.update(common_map) + result.append(stroke_map) + return result def _ParseStopList(self, parent_node): result = [] @@ -390,7 +458,8 @@ def _MaybeConvertShadow(self, has_shadow, output): output.WriteFloat(2.) output.WriteInt32(0xFF292929) - def _ConvertStyle(self, style, output): + def _ConvertStyleCommand(self, style, output): + """Converts style attribute.""" if style == 'fill': output.WriteByte(CMD_PICTURE_PAINT_STYLE) output.WriteByte(0) @@ -490,23 +559,57 @@ def _MaybeConvertStrokeLinejoin(self, stroke_linejoin, output): logging.critical('unknown stroke-linejoin: %s', stroke_linejoin) sys.exit(1) - def _ConvertStyleMap(self, style_map, output): - self._ConvertStyle(style_map['style'], output) - self._MaybeConvertColor(style_map.get('color'), output) - self._MaybeConvertShader(style_map.get('shader'), output) - self._MaybeConvertShadow(style_map['shadow'], output) - self._MaybeConvertStrokeWidth(style_map.get('stroke-width'), output) - self._MaybeConvertStrokeLinecap(style_map.get('stroke-linecap'), output) - self._MaybeConvertStrokeLinejoin(style_map.get('stroke-linejoin'), output) - output.WriteByte(CMD_PICTURE_PAINT_EOP) - - def _ConvertStyleList(self, style_list, output): - output.WriteByte(len(style_list)) - for style_map in style_list: - self._ConvertStyleMap(style_map, output) + def _MaybeConvertFontSize(self, font_size, output): + if font_size is None: + return + output.WriteByte(CMD_PICTURE_PAINT_FONT_SIZE) + output.WriteFloat(font_size) + return - def _ConvertStyleCategory(self, style_category, output): - output.WriteByte(1) + def _MaybeConvertTextAnchor(self, text_anchor, output): + if text_anchor is None: + return + output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR) + if text_anchor == 'start': + output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_START) + elif text_anchor == 'middle': + output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE) + elif text_anchor == 'end': + output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_END) + else: + logging.critical('text-anchor is invalid (%s)', text_anchor) + sys.exit(1) + return + + def _MaybeConvertDominantBaseline(self, baseline, output): + if baseline is None: + return + output.WriteByte(CMD_PICTURE_PAINT_DOMINANT_BASELINE) + if baseline == 'auto': + output.WriteByte(CMD_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO) + elif baseline == 'central': + output.WriteByte(CMD_PICTURE_PAINT_DOMINANTE_BASELINE_CENTRAL) + else: + logging.critical('dominant-baseline is invalid (%s)', baseline) + sys.exit(1) + return + + def _MaybeConvertFontWeight(self, weight, output): + if weight is None: + return + output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT) + if weight == 'normal': + output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT_NORMAL) + elif weight == 'bold': + output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT_BOLD) + else: + logging.critical('font-weight is invalid (%s)', weight) + sys.exit(1) + return + + def _MaybeConvertStyleCategory(self, style_category, output): + if style_category is None: + return for id_prefix, category in STYLE_CATEGORY_MAP: if style_category.startswith(id_prefix): output.WriteByte(STYLE_CATEGORY_TAG + category) @@ -514,6 +617,25 @@ def _ConvertStyleCategory(self, style_category, output): logging.critical('unknown style_category: "%s"', style_category) sys.exit(1) + def _ConvertStyle(self, style_category, style_list, output): + style_len = len(style_list) + output.WriteByte(style_len) + for style_map in style_list: + self._ConvertStyleCommand(style_map['style'], output) + self._MaybeConvertColor(style_map.get('color'), output) + self._MaybeConvertShader(style_map.get('shader'), output) + self._MaybeConvertShadow(style_map['shadow'], output) + self._MaybeConvertStrokeWidth(style_map.get('stroke-width'), output) + self._MaybeConvertStrokeLinecap(style_map.get('stroke-linecap'), output) + self._MaybeConvertStrokeLinejoin(style_map.get('stroke-linejoin'), output) + self._MaybeConvertFontSize(style_map.get('font-size'), output) + self._MaybeConvertTextAnchor(style_map.get('text-anchor'), output) + self._MaybeConvertDominantBaseline(style_map.get('dominant-baseline'), + output) + self._MaybeConvertFontWeight(style_map.get('font-weight'), output) + self._MaybeConvertStyleCategory(style_category, output) + output.WriteByte(CMD_PICTURE_PAINT_EOP) + def _ConvertPath(self, node, output): path = node.get('d') if path is None: @@ -650,10 +772,7 @@ def _ConvertPathElement( self, node, style_category, has_shadow, shader_map, output): style_list = self._ParseStyle(node, has_shadow, shader_map) self._ConvertPath(node, output) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) def _ConvertPolylineElement( self, node, style_category, has_shadow, shader_map, output): @@ -667,10 +786,7 @@ def _ConvertPolylineElement( output.WriteByte(len(point_list)) for coord in point_list: output.WriteFloat(coord) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) def _ConvertPolygonElement( self, node, style_category, has_shadow, shader_map, output): @@ -681,10 +797,7 @@ def _ConvertPolygonElement( output.WriteByte(len(point_list)) for coord in point_list: output.WriteFloat(coord) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) def _ConvertLineElement( self, node, style_category, has_shadow, shader_map, output): @@ -698,10 +811,7 @@ def _ConvertLineElement( output.WriteFloat(y1) output.WriteFloat(x2) output.WriteFloat(y2) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) def _ConvertCircleElement( self, node, style_category, has_shadow, shader_map, output): @@ -713,10 +823,7 @@ def _ConvertCircleElement( output.WriteFloat(cx) output.WriteFloat(cy) output.WriteFloat(r) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) def _ConvertEllipseElement( self, node, style_category, has_shadow, shader_map, output): @@ -730,10 +837,7 @@ def _ConvertEllipseElement( output.WriteFloat(cy) output.WriteFloat(rx) output.WriteFloat(ry) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) def _ConvertRectElement( self, node, style_category, has_shadow, shader_map, output): @@ -747,14 +851,39 @@ def _ConvertRectElement( output.WriteFloat(y) output.WriteFloat(w) output.WriteFloat(h) - if style_category is not None: - self._ConvertStyleCategory(style_category, output) - else: - self._ConvertStyleList(style_list, output) + self._ConvertStyle(style_category, style_list, output) - def _ConvertGroupElement( + def _ConvertTextElement( self, node, style_category, has_shadow, shader_map, output): + """Converts text element. + + The text element must not have any children. + Args: + node: node + style_category: style_category(optional) + has_shadow: shadow value + shader_map: shader map + output: output stream + """ + if not node.text: + # Ignore empty text node + return + + style_list = self._ParseStyle(node, has_shadow, shader_map) + x = float(node.get('x', 0)) + y = float(node.get('y', 0)) + output.WriteByte(CMD_PICTURE_DRAW_TEXT) + output.WriteFloat(x) + output.WriteFloat(y) + if node: + logging.critical(' with children is not supported.') + sys.exit(1) + output.WriteString(node.text) + self._ConvertStyle(style_category, style_list, output) + + def _ConvertGroupElement( + self, node, style_category, has_shadow, shader_map, output): transform = node.get('transform') if transform: # Output order of 3x3 matrix; @@ -794,6 +923,25 @@ def _ConvertGroupElement( output.WriteFloat(tx) output.WriteFloat(ty) output.WriteFloat(1) + elif transformation == 'scale': + parsed_coords = tuple(self._ParseFloatList(coords)) + if len(parsed_coords) != 1 and len(parsed_coords) != 2: + logging.critical('Invalid argument for scale: %s', coords) + sys.exit(1) + sx = parsed_coords[0] + if len(parsed_coords) == 1: + sy = sx + else: + sy = parsed_coords[1] + output.WriteFloat(sx) + output.WriteFloat(0) + output.WriteFloat(0) + output.WriteFloat(0) + output.WriteFloat(sy) + output.WriteFloat(0) + output.WriteFloat(0) + output.WriteFloat(0) + output.WriteFloat(1) else: # Never reach here. Just in case. logging.critical('Unsupported transform: %s', transform) @@ -855,17 +1003,26 @@ def _ConvertPictureSequence( node, style_category, has_shadow, shader_map, output) return + if node.tag == '{http://www.w3.org/2000/svg}text': + self._ConvertTextElement( + node, style_category, has_shadow, shader_map, output) + return + if node.tag in ['{http://www.w3.org/2000/svg}g', '{http://www.w3.org/2000/svg}svg']: self._ConvertGroupElement( node, style_category, has_shadow, shader_map, output) return + # Ignore following tags. if node.tag in ['{http://www.w3.org/2000/svg}linearGradient', - '{http://www.w3.org/2000/svg}radialGradient']: + '{http://www.w3.org/2000/svg}radialGradient', + '{http://www.w3.org/2000/svg}metadata', + '{http://www.w3.org/2000/svg}defs',]: return - logging.warning('Unknown element: %s', node.tag) + logging.critical('Unknown element: %s', node.tag) + sys.exit(1) def _OutputEOP(self, output): output.WriteByte(CMD_PICTURE_EOP) @@ -899,21 +1056,22 @@ def ConvertStateListDrawable(self, drawable_source_list): return output.output.getvalue() -def ConvertFiles(svg_dir, output_dir): +def ConvertFiles(svg_paths, output_dir): """Converts SVG files into MechaMozc specific *pic* files. Args: - svg_dir: Path to a directory which has svg files (recursively). + svg_paths: Comma separated paths to a directory/zip which has svg files + (recursively). output_dir: Path of the destination directory. """ - logging.debug('Start SVG conversion. From:%s, To:%s', svg_dir, output_dir) + logging.debug('Start SVG conversion. From:%s, To:%s', svg_paths, output_dir) # Ensure that the output directory exists. if not os.path.exists(output_dir): os.makedirs(output_dir) converter = MozcDrawableConverter() number_of_conversion = 0 - for dirpath, dirnames, filenames in os.walk(svg_dir): + for dirpath, _, filenames in util.WalkFileContainers(svg_paths): for filename in filenames: basename, ext = os.path.splitext(filename) if ext != '.svg': @@ -971,11 +1129,16 @@ def ConvertFiles(svg_dir, output_dir): def ParseOptions(): + """Parses options.""" parser = optparse.OptionParser() - parser.add_option('--svg_dir', dest='svg_dir', - help='Path to a directory containing .svg files.') + parser.add_option('--svg_paths', dest='svg_paths', + help='Comma separated paths to a directory or' + ' a .zip file containing .svg files.') parser.add_option('--output_dir', dest='output_dir', help='Path to the output directory,') + parser.add_option('--build_log', dest='build_log', + help='(Optional) Path to build log to generate.' + ' If set, nothing will be sent to stderr.') parser.add_option('--verbose', '-v', dest='verbose', action='store_true', default=False, help='Shows verbose message.') @@ -987,9 +1150,12 @@ def main(): logging.getLogger().addFilter(util.ColoredLoggingFilter()) options = ParseOptions() - if options.verbose: + if options.build_log: + logging.getLogger().handlers = [] + logging.getLogger().addHandler(logging.FileHandler(options.build_log, 'w')) + if options.verbose or options.build_log: logging.getLogger().setLevel(logging.DEBUG) - ConvertFiles(options.svg_dir, options.output_dir) + ConvertFiles(options.svg_paths, options.output_dir) if __name__ == '__main__': diff --git a/src/android/gen_subset_font.py b/src/android/gen_subset_font.py new file mode 100644 index 000000000..67db25832 --- /dev/null +++ b/src/android/gen_subset_font.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# Copyright 2010-2014, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Generates subset font. + +1. Corrects characters from .svg files in given directory. + Only text nodes in element are taken into account. +2. Make subset font which contains only the characters corrected above. +""" + +import logging +import optparse +import os +import shutil +import subprocess +import sys +import tempfile +from xml.etree import cElementTree as ElementTree + +from build_tools import util + + +def ExtractCharactersFromSvgFile(svg_file): + result = set() + for text in ElementTree.parse(svg_file).findall( + './/{http://www.w3.org/2000/svg}text'): + if text.text: + result.update(tuple(text.text)) # Split into characters. + logging.debug('%s: %s', svg_file, result) + return result + + +def ExtractCharactersFromDirectory(svg_paths): + result = set() + for dirpath, _, filenames in util.WalkFileContainers(svg_paths): + for filename in filenames: + if os.path.splitext(filename)[1] != '.svg': + # Do nothing for files other than svg. + continue + result.update(ExtractCharactersFromSvgFile( + os.path.join(dirpath, filename))) + return list(result) + + +def MakeSubsetFont(fonttools_path, unicodes, input_font, output_font): + """Makes subset font using fontTools toolchain. + + Args: + fonttools_path: Path to fontTools library. + unicodes: A list of unicode characters of which glyph should be in + the output. + input_font: Path to input font. + output_font: Path to output font. + """ + if not unicodes: + # subset.py requires at least one unicode character. + logging.debug('No unicode character is specified. Use "A" as stub.') + unicodes = [u'A'] + + tempdir = tempfile.mkdtemp() + try: + # fontTools's output file name is fixed (input file path + '.subset'). + # To get result with specified file name, copy the input font + # to temporary directory and copy the result with renaming. + shutil.copy(input_font, tempdir) + temp_input_font = os.path.join(tempdir, os.path.basename(input_font)) + commands = ['python', os.path.join(fonttools_path, 'subset.py'), + temp_input_font] + commands.extend(['U+%08x' % ord(char) for char in unicodes]) + env = os.environ.copy() + # In fontTools toolchain, "from fontTools import XXXX" is executed. + # In order for successfull import, add parent directory of the toolchain + # into PYTHONPATH. + env['PYTHONPATH'] += ':%s' % os.path.join(fonttools_path, '..') + if subprocess.call(commands, env=env) != 0: + sys.exit(1) + shutil.copyfile('%s.subset' % temp_input_font, output_font) + finally: + shutil.rmtree(tempdir) + + +def ParseOptions(): + """Parses options.""" + parser = optparse.OptionParser() + parser.add_option('--svg_paths', dest='svg_paths', + help='Comma separated paths to a directory or' + ' a .zip file containing .svg files.') + parser.add_option('--input_font', dest='input_font', + help='Path to the input font.') + parser.add_option('--output_font', dest='output_font', + help='Path to the output font.') + parser.add_option('--fonttools_path', dest='fonttools_path', + help='Path to fontTools toolchain.') + parser.add_option('--verbose', dest='verbose', + help='Verbosity') + return parser.parse_args()[0] + + +def main(): + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + + options = ParseOptions() + if options.verbose: + logging.getLogger().setLevel(logging.DEBUG) + unicodes = ExtractCharactersFromDirectory(options.svg_paths) + MakeSubsetFont(options.fonttools_path, unicodes, + options.input_font, options.output_font) + + +if __name__ == '__main__': + main() + diff --git a/src/android/proguard-project.txt b/src/android/proguard-project.txt index f401f1628..26c35b43e 100644 --- a/src/android/proguard-project.txt +++ b/src/android/proguard-project.txt @@ -54,6 +54,11 @@ public static ; } +# Skin's fields are accessed by reflection. +-keepclassmembers class org.mozc.android.inputmethod.japanese.view.Skin { + ; +} + # Needed for Guava library. -libraryjars libs/jsr305.jar -dontwarn sun.misc.Unsafe diff --git a/src/android/project.properties b/src/android/project.properties index 8d01942d4..8cef1f331 100644 --- a/src/android/project.properties +++ b/src/android/project.properties @@ -30,7 +30,7 @@ # Project target. -target=android-19 +target=android-21 # Location of the ProGuard configuration. proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt @@ -38,5 +38,5 @@ proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard # Library projects # 'resources' project must not be placed at the tail of reference definition. # Otherwise aapt command crashes. -android.library.reference.1=static_resources/resources_oss +android.library.reference.1=resources android.library.reference.2=protobuf diff --git a/src/android/protobuf/project.properties b/src/android/protobuf/project.properties index 9aacf2f19..c3b6d82bf 100644 --- a/src/android/protobuf/project.properties +++ b/src/android/protobuf/project.properties @@ -39,4 +39,4 @@ android.library=true # Project target. -target=android-19 +target=android-21 diff --git a/src/android/static_resources/resources_oss/AndroidManifest.xml b/src/android/resources/AndroidManifest.xml similarity index 99% rename from src/android/static_resources/resources_oss/AndroidManifest.xml rename to src/android/resources/AndroidManifest.xml index e3917615b..b060f879e 100644 --- a/src/android/static_resources/resources_oss/AndroidManifest.xml +++ b/src/android/resources/AndroidManifest.xml @@ -32,5 +32,5 @@ - + diff --git a/src/android/static_resources/resources_oss/ant.properties b/src/android/resources/ant.properties similarity index 100% rename from src/android/static_resources/resources_oss/ant.properties rename to src/android/resources/ant.properties diff --git a/src/android/static_resources/resources_oss/build.xml b/src/android/resources/build.xml similarity index 100% rename from src/android/static_resources/resources_oss/build.xml rename to src/android/resources/build.xml diff --git a/src/android/resources/proguard-project.txt b/src/android/resources/proguard-project.txt new file mode 100644 index 000000000..5317df3fe --- /dev/null +++ b/src/android/resources/proguard-project.txt @@ -0,0 +1,49 @@ +# Copyright 2010-2014, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/src/android/static_resources/resources_oss/project.properties b/src/android/resources/project.properties similarity index 99% rename from src/android/static_resources/resources_oss/project.properties rename to src/android/resources/project.properties index bc023491c..613d5c200 100644 --- a/src/android/static_resources/resources_oss/project.properties +++ b/src/android/resources/project.properties @@ -40,5 +40,5 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-19 +target=android-21 android.library=true diff --git a/src/android/resources/resources.gyp b/src/android/resources/resources.gyp new file mode 100644 index 000000000..fa6b0b92c --- /dev/null +++ b/src/android/resources/resources.gyp @@ -0,0 +1,304 @@ +# Copyright 2010-2014, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +{ + 'variables': { + 'relative_dir': 'android/resources', + 'abs_android_dir': '<(abs_depth)/<(relative_dir)', + # Actions with an existing input and non-existing output behave like + # phony rules. Nothing matters for an input but its existence, so + # we use 'resources.gyp' as a dummy input since it must exist. + 'dummy_input_file': 'resources.gyp', + # GYP's 'copies' rule cannot copy a whole directory recursively, so we use + # our own script to copy files. + 'copy_file': ['python', '../../build_tools/copy_file.py'], + 'shared_intermediate_mozc_dir': '<(SHARED_INTERMEDIATE_DIR)/', + 'static_resources_dir': '<(abs_depth)/android/static_resources', + 'sdk_resources_dir': '<(shared_intermediate_mozc_dir)/android/resources', + }, + 'conditions': [ + ['branding=="GoogleJapaneseInput"', { + 'conditions': [ + ['android_hide_icon==1', { + 'variables': { + 'launcher_icon_bools': '<(static_resources_dir)/launcher_icon_resources/launcher_icon_preinstall_bools.xml', + }, + }, { # else + 'variables': { + 'launcher_icon_bools': '<(static_resources_dir)/launcher_icon_resources/launcher_icon_standard_bools.xml', + }, + }], + ['android_release_icon==1', { + 'variables': { + 'application_icon_dir': '<(static_resources_dir)/application_icon/release_icon/', + }, + }, { # else + 'variables': { + 'application_icon_dir': '<(static_resources_dir)/application_icon/dogfood_icon/', + }, + }], + ], + 'variables': { + 'original_sdk_resources_dir': '<(static_resources_dir)/resources', + }, + }, { # 'branding!="GoogleJapaneseInput"' + 'variables': { + 'launcher_icon_bools': '<(static_resources_dir)/launcher_icon_resources/launcher_icon_standard_bools.xml', + 'original_sdk_resources_dir': '<(static_resources_dir)/resources_oss', + 'application_icon_dir': '<(static_resources_dir)/application_icon/oss_icon/', + }, + }], + ], + 'targets': [ + { + 'target_name': 'resources', + 'type': 'none', + 'dependencies': [ + 'resources_project', + ], + 'actions': [ + { + 'action_name': 'build_resources', + 'inputs': [ + 'AndroidManifest.xml', + 'build.xml', + 'project.properties', + 'ant.properties', + 'proguard-project.txt', + ], + 'outputs': [ + 'bin/classes.jar', + 'gen/org/mozc/android/inputmethod/japanese/resources/R.java', + ], + 'includes': ['../ant.gypi'], + }, + ], + }, + { + 'target_name': 'resources_project', + 'type': 'none', + 'dependencies': [ + 'copy_resources', + 'gen_kcm_data', + 'gen_mozc_drawable', + ], + }, + { + # Copies original resources to intermediate directory. + # Then make symbolic link from android/resources to the intermediate directory. + # The symbolic link is required to build both by ADT and ant. + 'target_name': 'copy_resources', + 'type': 'none', + 'includes': ['../android_resources.gypi'], + 'inputs': ['<@(android_resources)'], + 'outputs': ['dummy_copy_resources'], + 'actions': [ + { + 'action_name': 'copy_files', + 'inputs': ['<@(android_resources)'], + 'outputs': ['dummy_copy_files'], + 'action': [ + '<@(copy_file)', '-pr', + '<@(android_resources)', + '<(sdk_resources_dir)', + '--src_base', '<(original_sdk_resources_dir)', + ], + }, + { + # Make sure the link of resources/res does not exist. + # the new link is created in the next step. + 'action_name': 'remove_symbolic_link', + 'inputs': [ + 'dummy_copy_files' + ], + 'outputs': [ + 'dummy_remove_symbolic_link', + ], + 'action': [ + 'rm', '-f', 'res', + ], + }, + { + 'action_name': 'make_symbolic_link', + 'inputs': [ + 'dummy_remove_symbolic_link' + ], + 'outputs': [ + 'dummy_make_symbolic_link', + ], + 'action': [ + 'ln', '-r', '-s', '-f', + '<(sdk_resources_dir)/res', + 'res', + ], + }, + { + 'action_name': 'copy_configuration_dependent_resources', + 'inputs': [ + 'dummy_make_symbolic_link', + ], + 'outputs': [ + 'dummy_copy_configuration_dependent_resources', + ], + 'action': [ + 'ln', '-s', '-f', + '<(launcher_icon_bools)', + '<(sdk_resources_dir)/res/values/', + ], + }, + { + 'action_name': 'copy_application_icons', + 'variables': { + 'icon_files': [ + '<(application_icon_dir)/drawable-hdpi/application_icon.png', + '<(application_icon_dir)/drawable-mdpi/application_icon.png', + '<(application_icon_dir)/drawable-xhdpi/application_icon.png', + '<(application_icon_dir)/drawable-xxhdpi/application_icon.png', + '<(application_icon_dir)/drawable-xxxhdpi/application_icon.png', + ], + }, + 'inputs': [ + 'dummy_make_symbolic_link', + '<@(icon_files)', + ], + 'outputs': [ + '<(sdk_resources_dir)/res/drawable-hdpi/application_icon.png', + '<(sdk_resources_dir)/res/drawable-mdpi/application_icon.png', + '<(sdk_resources_dir)/res/drawable-xhdpi/application_icon.png', + '<(sdk_resources_dir)/res/drawable-xxhdpi/application_icon.png', + '<(sdk_resources_dir)/res/drawable-xxxhdpi/application_icon.png', + ], + 'action': ['<@(copy_file)', '-pr', + '<@(icon_files)', + '<(sdk_resources_dir)/res/', + '--src_base', '<(application_icon_dir)'], + }, + ], + }, + { + 'target_name': 'gen_kcm_data', + 'type': 'none', + 'dependencies': ['copy_resources'], + 'copies': [{ + 'destination': '<(sdk_resources_dir)/res/raw', + 'files': [ + '../../data/android/keyboard_layout_japanese109a.kcm', + ], + }], + }, + { + # This (and 'copy_asis_svg') tareget outputs single .zip file + # in order to make build system work correctly. + # Without this hack, all the names of the generated files + # should be listed up in 'inputs' and/or 'outputs' field. + 'target_name': 'transform_template_svg', + 'type': 'none', + 'actions': [ + { + 'action_name': 'transform_template_svg', + 'inputs': [ + '../../data/images/android/template/transform.py', + ], + 'outputs': [ + '<(shared_intermediate_mozc_dir)/data/images/android/svg/transformed.zip', + ], + 'conditions': [ + ['OS=="linux" or OS=="mac"', { + 'inputs': [' getLastLaunchAbiIndependentVersionCode() { - if (!this.sharedPreferences.contains(PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE)) { + if (!this.sharedPreferences.contains( + PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE)) { return Optional.absent(); } return Optional.of( - this.sharedPreferences.getInt(PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, 0)); + this.sharedPreferences.getInt( + PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, 0)); } @Override @@ -149,10 +150,45 @@ private ApplicationInitializer( this.telephonyManager = Preconditions.checkNotNull(telephonyManager); } - public Optional initialize(boolean omitWelcomeActivity, + /** + * Initializes the application. + * + *

Updates some preferences. + * Here we use three preference items. + *

    + *
  • pref_welcome_activity_shown: True if the "Welcome" activity has shown at least once. + *
  • pref_last_launch_abi_independent_version_code: The latest version number which + * has launched at least once. + *
  • pref_launched_at_least_once: Deprecated. True if the the IME has launched at least once. + *
+ * Some preferences should be set at the first time launch. + * If the IME is a system application (preinstalled), it shouldn't show "Welcome" activity. + * If an update is performed (meaning that the IME becomes non-system app), + * the activity should be shown at the first time launch. + * + * We have to do migration process. + * If pref_launched_at_least_once exists, pref_welcome_activity_shown is recognized as + * true and pref_last_launch_abi_independent_version_code is recognized as + * LAUNCHED_AT_LEAST_ONCE_DEPRECATED_VERSION_CODE. And then pref_launched_at_least_once is + * removed. + * + * @param isSystemApplication true if the app is a system application (== preinstall) + * @param isDevChannel true if the app is built for dev channel + * @param isWelcomeActivityPreferred true if the configuration prefers to shown welcome activity + * if it's not been shown yet. + * @param abiIndependentVersionCode ABI independent version code, typically obtained + * from {@link MozcUtil#getAbiIndependentVersionCode(Context)} + * + * @return if forwarding is needed Intent is returned. The caller side should invoke the Intent. + */ + public Optional initialize(boolean isSystemApplication, boolean isDevChannel, boolean isWelcomeActivityPreferred, - int abiIndependentVersionCode) { + int abiIndependentVersionCode, + LauncherIconManager launcherIconManager, + PreferenceManagerStaticInterface preferenceManager) { + Preconditions.checkNotNull(launcherIconManager); + Preconditions.checkNotNull(preferenceManager); SharedPreferences.Editor editor = sharedPreferences.edit(); Resources resources = context.getResources(); try { @@ -175,27 +211,43 @@ public Optional initialize(boolean omitWelcomeActivity, // Preferences: Update if this is the first launch if (!lastVersionCode.isPresent()) { // Store full-screen relating preferences. + DisplayMetrics portraitMetrics = getPortraitDisplayMetrics( + resources.getDisplayMetrics(), resources.getConfiguration().orientation); storeDefaultFullscreenMode( - sharedPreferences, - getPortraitDisplayMetrics(resources.getDisplayMetrics(), - resources.getConfiguration().orientation), - resources.getDimension(R.dimen.fullscreen_threshold), - resources.getDimension(R.dimen.ime_window_height_portrait), - resources.getDimension(R.dimen.ime_window_height_landscape)); + sharedPreferences, portraitMetrics.heightPixels, portraitMetrics.widthPixels, + (int) Math.ceil(getDimensionForOrientation( + resources, R.dimen.input_frame_height, Configuration.ORIENTATION_PORTRAIT)), + (int) Math.ceil(getDimensionForOrientation( + resources, R.dimen.input_frame_height, Configuration.ORIENTATION_LANDSCAPE)), + resources.getDimensionPixelOffset(R.dimen.fullscreen_threshold)); // Run emoji provider type detection, so that the detected provider will be // used as the default values of the preference activity. EmojiProviderType.maybeSetDetectedEmojiProviderType( sharedPreferences, telephonyManager); } + // Update launcher icon visibility and relating preference. + launcherIconManager.updateLauncherIconVisibility(context); + // Save default preference to the storage. + // NOTE: This method must NOT be called before updateLauncherIconVisibility() above. + // Above method requires PREF_LAUNCHER_ICON_VISIBILITY_KEY is not filled with + // the default value. + // If PREF_LAUNCHER_ICON_VISIBILITY_KEY is filled prior to + // updateLauncherIconVisibility(), the launcher icon will be unexpectedly shown + // when 2.16.1955.3 (preinstall version) is overwritten by PlayStore version. + PreferenceUtil.setDefaultValues( + preferenceManager, context, MozcUtil.isDebug(context), + resources.getBoolean(R.bool.sending_information_features_enabled)); if (isDevChannel) { // Usage Stats: Make pref_other_usage_stats_key enabled when dev channel. editor.putBoolean(PreferenceUtil.PREF_OTHER_USAGE_STATS_KEY, true); + maybeShowNotificationForDevChannel(abiIndependentVersionCode, + lastVersionCode); } // Welcome Activity - if (!isActivityShown && !omitWelcomeActivity && isWelcomeActivityPreferred) { + if (!isActivityShown && !isSystemApplication && isWelcomeActivityPreferred) { editor.putBoolean(PREF_WELCOME_ACTIVITY_SHOWN, true); Intent intent = new Intent(context, FirstTimeLaunchActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -204,18 +256,21 @@ public Optional initialize(boolean omitWelcomeActivity, return Optional.absent(); } finally { editor.remove(PREF_LAUNCHED_AT_LEAST_ONCE); - editor.putInt(PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, abiIndependentVersionCode); + editor.putInt(PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, + abiIndependentVersionCode); editor.commit(); } } + private void maybeShowNotificationForDevChannel( + int abiIndependentVersionCode, Optional lastVersionCode) { + } + /** * Returns a modified {@code DisplayMetrics} which equals to portrait modes's one. * * If current orientation is PORTRAIT, given {@code currentMetrics} is returned. * Otherwise {@code currentMetrics}'s {@code heightPixels} and {@code widthPixels} are swapped. - * - * Package private for testing purpose. */ @VisibleForTesting static DisplayMetrics getPortraitDisplayMetrics(DisplayMetrics currentMetrics, @@ -231,28 +286,44 @@ static DisplayMetrics getPortraitDisplayMetrics(DisplayMetrics currentMetrics, return result; } + /** + * Get a dimension for the specified orientation. + * This method may be heavy since it updates the {@code resources} twice. + */ + @VisibleForTesting + static float getDimensionForOrientation(Resources resources, int id, int orientation) { + Configuration configuration = resources.getConfiguration(); + if (configuration.orientation == orientation) { + return resources.getDimension(id); + } + + Configuration originalConfiguration = new Configuration(resources.getConfiguration()); + try { + configuration.orientation = orientation; + resources.updateConfiguration(configuration, null); + return resources.getDimension(id); + } finally { + resources.updateConfiguration(originalConfiguration, null); + } + } + /** * Stores the default value of "fullscreen mode" to the shared preference. - * - * Package private for testing purpose. */ @VisibleForTesting static void storeDefaultFullscreenMode( - SharedPreferences sharedPreferences, DisplayMetrics displayMetrics, - float fullscreenThresholdInPixel, - float portraitImeHeightInPixel, float landscapeImeHeightInPixel) { + SharedPreferences sharedPreferences, + int portraitDisplayHeight, int landscapeDisplayHeight, + int portraitInputFrameHeight, int landscapeInputFrameHeight, int fullscreenThreshold) { Preconditions.checkNotNull(sharedPreferences); - Preconditions.checkNotNull(displayMetrics); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putBoolean( "pref_portrait_fullscreen_key", - displayMetrics.heightPixels - portraitImeHeightInPixel - < fullscreenThresholdInPixel); + portraitDisplayHeight - portraitInputFrameHeight < fullscreenThreshold); editor.putBoolean( "pref_landscape_fullscreen_key", - displayMetrics.widthPixels - landscapeImeHeightInPixel - < fullscreenThresholdInPixel); + landscapeDisplayHeight - landscapeInputFrameHeight < fullscreenThreshold); editor.commit(); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java b/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java index 74ceecd18..8e3ac7f09 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java @@ -29,6 +29,7 @@ package org.mozc.android.inputmethod.japanese; +import org.mozc.android.inputmethod.japanese.MozcView.InputFrameFoldButtonClickListener; import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; @@ -40,19 +41,22 @@ import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.DescriptionLayoutPolicy; import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.ValueScalingPolicy; import org.mozc.android.inputmethod.japanese.ui.ConversionCandidateLayouter; +import org.mozc.android.inputmethod.japanese.ui.InputFrameFoldButtonView; import org.mozc.android.inputmethod.japanese.ui.ScrollGuideView; import org.mozc.android.inputmethod.japanese.ui.SpanFactory; -import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import android.content.Context; import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; import android.view.animation.Animation; -import android.widget.CompoundButton; +import android.widget.LinearLayout; /** * The view to show candidates. @@ -60,17 +64,13 @@ */ public class CandidateView extends InOutAnimatedFrameLayout implements MemoryManageable { - /** - * Adapter for conversion candidate selection. - */ + /** Adapter for conversion candidate selection. */ + @VisibleForTesting static class ConversionCandidateSelectListener implements CandidateSelectListener { private final ViewEventListener viewEventListener; ConversionCandidateSelectListener(ViewEventListener viewEventListener) { - if (viewEventListener == null) { - throw new NullPointerException("viewEventListener should be non-null."); - } - this.viewEventListener = viewEventListener; + this.viewEventListener = Preconditions.checkNotNull(viewEventListener); } @Override @@ -80,7 +80,6 @@ public void onCandidateSelected(CandidateWord candidateWord, Optional r } } - private class OutAnimationAdapter extends AnimationAdapter { @Override public void onAnimationEnd(Animation animation) { @@ -94,6 +93,8 @@ static class ConversionCandidateWordView extends CandidateWordView { private static final String DESCRIPTION_DELIMITER = " \t\n\r\f"; ScrollGuideView scrollGuideView = null; + InputFrameFoldButtonView inputFrameFoldButtonView = null; + @VisibleForTesting int foldButtonBackgroundVisibilityThreshold = 0; // TODO(hidehiko): Simplify the interface as this is needed just for expandSuggestion. private ViewEventListener viewEventListener; @@ -104,7 +105,7 @@ static class ConversionCandidateWordView extends CandidateWordView { private boolean isExpanded = false; { - setBackgroundDrawableType(DrawableType.CANDIDATE_BACKGROUND); + setSpanBackgroundDrawableType(DrawableType.CANDIDATE_BACKGROUND); layouter = new ConversionCandidateLayouter(); } @@ -117,6 +118,7 @@ public ConversionCandidateWordView(Context context, AttributeSet attributeSet) { resources.getInteger(R.integer.candidate_scroller_minimum_velocity)); } + @VisibleForTesting void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) { Preconditions.checkArgument(candidateTextSize > 0); Preconditions.checkArgument(descriptionTextSize > 0); @@ -130,7 +132,9 @@ void setCandidateTextDimension(float candidateTextSize, float descriptionTextSiz resources.getDimension(R.dimen.symbol_description_right_padding); float descriptionVerticalPadding = resources.getDimension(R.dimen.symbol_description_bottom_padding); + float separatorWidth = resources.getDimensionPixelSize(R.dimen.candidate_separator_width); + carrierEmojiRenderHelper.setCandidateTextSize(candidateTextSize); candidateLayoutRenderer.setValueTextSize(candidateTextSize); candidateLayoutRenderer.setValueHorizontalPadding(valueHorizontalPadding); candidateLayoutRenderer.setValueScalingPolicy(ValueScalingPolicy.HORIZONTAL); @@ -138,6 +142,7 @@ void setCandidateTextDimension(float candidateTextSize, float descriptionTextSiz candidateLayoutRenderer.setDescriptionHorizontalPadding(descriptionHorizontalPadding); candidateLayoutRenderer.setDescriptionVerticalPadding(descriptionVerticalPadding); candidateLayoutRenderer.setDescriptionLayoutPolicy(DescriptionLayoutPolicy.EXCLUSIVE); + candidateLayoutRenderer.setSeparatorWidth(separatorWidth); SpanFactory spanFactory = new SpanFactory(); spanFactory.setValueTextSize(candidateTextSize); @@ -160,6 +165,8 @@ void setCandidateTextDimension(float candidateTextSize, float descriptionTextSiz layouter.setValueHeight(candidateTextSize); layouter.setValueHorizontalPadding(valueHorizontalPadding); layouter.setValueVerticalPadding(valueVerticalPadding); + + foldButtonBackgroundVisibilityThreshold = (int) (1.8 * valueVerticalPadding); } @Override @@ -171,7 +178,9 @@ void setViewEventListener(ViewEventListener viewEventListener) { this.viewEventListener = viewEventListener; } + @Override void reset() { + super.reset(); isExpanded = false; } @@ -179,6 +188,10 @@ void reset() { protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) { super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY); updateScrollGuide(); + if (inputFrameFoldButtonView != null) { + inputFrameFoldButtonView.showBackgroundForScrolled( + scrollY > foldButtonBackgroundVisibilityThreshold); + } expandSuggestionIfNeeded(); } @@ -228,6 +241,17 @@ void updateForExpandSuggestion(CandidateList candidateList) { super.update(candidateList); updateScrollGuide(); } + + @Override + protected Drawable getViewBackgroundDrawable(Skin skin) { + return skin.conversionCandidateViewBackgroundDrawable; + } + + @Override + public void setSkin(Skin skin) { + super.setSkin(skin); + candidateLayoutRenderer.setSeparatorColor(skin.candidateBackgroundSeparatorColor); + } } public CandidateView(Context context) { @@ -250,32 +274,36 @@ public void onFinishInflate() { ConversionCandidateWordView conversionCandidateWordView = getConversionCandidateWordView(); scrollGuideView.setScroller(conversionCandidateWordView.scroller); conversionCandidateWordView.scrollGuideView = scrollGuideView; - - // Initialize inputFrameFoldButton. - CompoundButton inputFrameFoldButton = getInputFrameFoldButton(); - inputFrameFoldButton.setBackgroundDrawable( - new MozcDrawableFactory(getResources()).getDrawable(R.raw.keyboard__fold__tab).orNull()); + conversionCandidateWordView.inputFrameFoldButtonView = getInputFrameFoldButton(); + // To use Canvas#drawPicture(), the view shouldn't be h/w accelerated. + getInputFrameFoldButton().setLayerType(View.LAYER_TYPE_SOFTWARE, null); reset(); } - CompoundButton getInputFrameFoldButton() { - return CompoundButton.class.cast(findViewById(R.id.input_frame_fold_button)); + @VisibleForTesting InputFrameFoldButtonView getInputFrameFoldButton() { + return InputFrameFoldButtonView.class.cast(findViewById(R.id.input_frame_fold_button)); } - ConversionCandidateWordView getConversionCandidateWordView() { + @VisibleForTesting ConversionCandidateWordView getConversionCandidateWordView() { return ConversionCandidateWordView.class.cast(findViewById(R.id.candidate_word_view)); } - ScrollGuideView getScrollGuideView() { + private ConversionCandidateWordContainerView getConversionCandidateWordContainerView() { + return ConversionCandidateWordContainerView.class.cast( + findViewById(R.id.conversion_candidate_word_container_view)); + } + + @VisibleForTesting ScrollGuideView getScrollGuideView() { return ScrollGuideView.class.cast(findViewById(R.id.candidate_scroll_guide_view)); } - /** - * Updates the view based on {@code Command}. - * Exposed as protected for testing purpose. - */ - protected void update(Command outCommand) { + @VisibleForTesting LinearLayout getCandidateWordFrame() { + return LinearLayout.class.cast(findViewById(R.id.candidate_word_frame)); + } + + /** Updates the view based on {@code Command}. */ + void update(Command outCommand) { if (outCommand == null) { getConversionCandidateWordView().update(null); return; @@ -283,8 +311,8 @@ protected void update(Command outCommand) { Input input = outCommand.getInput(); CandidateList allCandidateWords = outCommand.getOutput().getAllCandidateWords(); - if (input.getType() == CommandType.SEND_COMMAND && - input.getCommand().getType() == SessionCommand.CommandType.EXPAND_SUGGESTION) { + if (input.getType() == CommandType.SEND_COMMAND + && input.getCommand().getType() == SessionCommand.CommandType.EXPAND_SUGGESTION) { getConversionCandidateWordView().updateForExpandSuggestion(allCandidateWords); } else { getConversionCandidateWordView().update(allCandidateWords); @@ -293,19 +321,18 @@ protected void update(Command outCommand) { /** * Register callback object. - * Note: exposed as a protected method for testing purpose. * @param listener */ - protected void setViewEventListener(ViewEventListener listener, - OnClickListener inputFrameFoldButtonClickListner) { - if (listener == null) { - throw new NullPointerException("lister must be non-null."); - } + void setViewEventListener(ViewEventListener listener) { + Preconditions.checkNotNull(listener); ConversionCandidateWordView conversionCandidateWordView = getConversionCandidateWordView(); conversionCandidateWordView.setViewEventListener(listener); conversionCandidateWordView.setCandidateSelectListener( new ConversionCandidateSelectListener(listener)); - getInputFrameFoldButton().setOnClickListener(inputFrameFoldButtonClickListner); + } + + void setInputFrameFoldButtonOnClickListener(InputFrameFoldButtonClickListener listener) { + getInputFrameFoldButton().setOnClickListener(Preconditions.checkNotNull(listener)); } void reset() { @@ -317,9 +344,14 @@ void setInputFrameFoldButtonChecked(boolean checked) { getInputFrameFoldButton().setChecked(checked); } - void setSkinType(SkinType skinType) { - getScrollGuideView().setSkinType(skinType); - getConversionCandidateWordView().setSkinType(skinType); + @SuppressWarnings("deprecation") + void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + getScrollGuideView().setSkin(skin); + getConversionCandidateWordView().setSkin(skin); + getInputFrameFoldButton().setSkin(skin); + getCandidateWordFrame().setBackgroundColor(skin.candidateBackgroundBottomColor); + invalidate(); } void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) { @@ -328,12 +360,13 @@ void setCandidateTextDimension(float candidateTextSize, float descriptionTextSiz getConversionCandidateWordView().setCandidateTextDimension(candidateTextSize, descriptionTextSize); + getConversionCandidateWordContainerView().setCandidateTextDimension(candidateTextSize); } - void setNarrowMode(boolean narrowMode) { - getInputFrameFoldButton().setVisibility(narrowMode ? GONE : VISIBLE); + void enableFoldButton(boolean enabled) { + getInputFrameFoldButton().setVisibility(enabled ? VISIBLE : GONE); getConversionCandidateWordView().getCandidateLayouter() - .reserveEmptySpanForInputFoldButton(!narrowMode); + .reserveEmptySpanForInputFoldButton(enabled); } @Override diff --git a/src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java b/src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java new file mode 100644 index 000000000..6b875f907 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java @@ -0,0 +1,456 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese; + +import org.mozc.android.inputmethod.japanese.InOutAnimatedFrameLayout.VisibilityChangeListener; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; +import org.mozc.android.inputmethod.japanese.view.Skin; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.TranslateAnimation; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorInfo; + +/** + * Manages candidate views (floating, on-keyboard). + */ +class CandidateViewManager implements MemoryManageable { + + /** Listener interface to handle the height change of a keyboard candidate view. */ + public interface KeyboardCandidateViewHeightListener { + public void onExpanded(); + public void onCollapse(); + } + + private static class ClearCandidateAnimationListener implements Animation.AnimationListener { + private final CandidateView candidateView; + + public ClearCandidateAnimationListener(CandidateView candidateView) { + this.candidateView = Preconditions.checkNotNull(candidateView); + } + + @Override + public void onAnimationEnd(Animation animation) { + candidateView.update(EMPTY_COMMAND); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + } + + /** {@link CandidateMode#FLOATING} is only available on Lollipop or later. */ + private enum CandidateMode { + KEYBOARD, NUMBER, FLOATING, + } + + private static final Animation NO_ANIMATION = new Animation() {}; + @VisibleForTesting static final Command EMPTY_COMMAND = Command.getDefaultInstance(); + + @VisibleForTesting final CandidateView keyboardCandidateView; + @VisibleForTesting final FloatingCandidateView floatingCandidateView; + /** + * SymbolInputView which number candidate view belongs to is created lazily. + * Therefore number candidate view is not accessible when CandidateViewManager is instantiated. + */ + @VisibleForTesting Optional numberCandidateView = Optional.absent(); + + private Optional keyboardCandidateViewHeightListener = + Optional.absent(); + /** + * Current active candidate view. + * {@link CandidateMode#FLOATING} is only available on Lollipop or later. + */ + private CandidateMode candidateMode = CandidateMode.KEYBOARD; + + /** Cache of {@link EditorInfo} instance to switch candidate views. */ + private EditorInfo editorInfo = new EditorInfo(); + /** Cache of {@link Skin} instance to switch candidate views. */ + private Skin skin = Skin.getFallbackInstance(); + /** Cache of candidate text size. */ + private float candidateTextSize; + /** Cache of description text size. */ + private float descriptionTextSize; + /** Cache of {@link ViewEventListener}. */ + private Optional viewEventListener = Optional.absent(); + /** Cache of {@link VisibilityChangeListener}. */ + private Optional onVisibilityChangeListener = Optional.absent(); + /** + * True if extracted mode (== fullscreen mode) is activated. + *

+ * On extracted mode, floating candidate should be disabled in order to show extracted view + * in the screen. + */ + private boolean isExtractedMode = false; + private boolean allowFloatingMode = false; + private boolean narrowMode = false; + + /** + * Cache of {@link CursorAnchorInfo} instance to switch candidate views. + * This field is null if and only-if Floating candidate view is NOT available, so we don't mark + * this value as {code @Nullable} since this field should NOT be used in that situation. + */ + private CursorAnchorInfo cursorAnchorInfo; + + private Animation numberCandidateViewInAnimation = NO_ANIMATION; + private Animation numberCandidateViewOutAnimation = NO_ANIMATION; + + @SuppressLint("NewApi") + public CandidateViewManager( + CandidateView keyboardCandidateView, FloatingCandidateView floatingCandidateView) { + this.keyboardCandidateView = Preconditions.checkNotNull(keyboardCandidateView); + this.floatingCandidateView = Preconditions.checkNotNull(floatingCandidateView); + if (FloatingCandidateView.isAvailable()) { + cursorAnchorInfo = new CursorAnchorInfo.Builder().build(); + } + + keyboardCandidateView.setOutAnimationListener( + new ClearCandidateAnimationListener(keyboardCandidateView)); + } + + public void setNumberCandidateView(CandidateView numberCandidateView) { + this.numberCandidateView = Optional.of(numberCandidateView); + numberCandidateView.setSkin(skin); + numberCandidateView.enableFoldButton(true); + numberCandidateView.setInAnimation(numberCandidateViewInAnimation); + numberCandidateView.setOutAnimation(numberCandidateViewOutAnimation); + numberCandidateView.setOutAnimationListener( + new ClearCandidateAnimationListener(numberCandidateView)); + if (candidateTextSize > 0 && descriptionTextSize > 0) { + numberCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize); + } + if (viewEventListener.isPresent()) { + numberCandidateView.setViewEventListener(viewEventListener.get()); + } + numberCandidateView.setOnVisibilityChangeListener(onVisibilityChangeListener.orNull()); + } + + /** + * Updates the candidate views by {@code outCommand} and may invoke some animations. + *

+ * On-keyboard candidate view may animate and the animation listener may be invoked. + */ + public void update(Command outCommand) { + updateInternal(Preconditions.checkNotNull(outCommand), true); + } + + private void updateWithoutAnimation(Command outCommand) { + updateInternal(outCommand, false); + } + + private void updateInternal(Command outCommand, boolean withAnimation) { + if (candidateMode == CandidateMode.FLOATING) { + floatingCandidateView.setCandidates(outCommand); + return; + } + + Preconditions.checkState( + candidateMode == CandidateMode.KEYBOARD + || (candidateMode == CandidateMode.NUMBER && numberCandidateView.isPresent())); + CandidateView candidateView = (candidateMode == CandidateMode.KEYBOARD) + ? keyboardCandidateView : numberCandidateView.get(); + + if (withAnimation) { + if (hasCandidates(outCommand)) { + candidateView.update(outCommand); + // Call CandidateView#update only if there are some candidates in the output. + // In such case the candidate view will clear its canvas. + startKeyboardCandidateViewInAnimation(); + } else { + // We don't call update method here and clear candidates at the end of this animation, + // because it will clear the view's contents during the animation. + startKeyboardCandidateViewOutAnimation(); + } + } else { + candidateView.update(outCommand); + if (hasCandidates(outCommand)) { + candidateView.setVisibility(View.VISIBLE); + } else { + candidateView.setVisibility(View.GONE); + } + } + } + + public void setOnVisibilityChangeListener(Optional listener) { + this.onVisibilityChangeListener = Preconditions.checkNotNull(listener); + keyboardCandidateView.setOnVisibilityChangeListener(listener.orNull()); + if (numberCandidateView.isPresent()) { + numberCandidateView.get().setOnVisibilityChangeListener(listener.orNull()); + } + } + + /** + * Enables/Disables a floating candidate view. + *

+ * This method turned floating mode on if it is preferred. + * The floating candidate view is only available on Lollipop or later. + */ + private void updateCandiadateWindowActivation() { + boolean floatingMode = narrowMode && allowFloatingMode && !isExtractedMode; + if (floatingMode == (candidateMode == CandidateMode.FLOATING)) { + return; + } + + // Clears candidates on the current candidate window. + updateWithoutAnimation(EMPTY_COMMAND); + + // Updates the other candidate view. + candidateMode = floatingMode ? CandidateMode.FLOATING : CandidateMode.KEYBOARD; + updateWithoutAnimation(EMPTY_COMMAND); + setEditorInfo(editorInfo); + if (FloatingCandidateView.isAvailable()) { + setCursorAnchorInfo(cursorAnchorInfo); + } + // In order to show extracted view correctly, make the visibility GONE when it is not activated. + floatingCandidateView.setVisibility(floatingMode ? View.VISIBLE : View.GONE); + } + + public void setAllowFloatingMode(boolean allowFloatingMode) { + Preconditions.checkArgument(!allowFloatingMode || FloatingCandidateView.isAvailable()); + this.allowFloatingMode = allowFloatingMode; + updateCandiadateWindowActivation(); + } + + public void setNarrowMode(boolean narrowMode) { + this.narrowMode = narrowMode; + keyboardCandidateView.enableFoldButton(!narrowMode); + updateCandiadateWindowActivation(); + } + + public void setNumberMode(boolean numberMode) { + CandidateMode nextMode = numberMode ? CandidateMode.NUMBER : CandidateMode.KEYBOARD; + if (candidateMode == nextMode) { + return; + } + if (nextMode == CandidateMode.NUMBER) { + // Hide keyboard candidate view since it is higher than symbol view. + updateWithoutAnimation(EMPTY_COMMAND); + } + candidateMode = nextMode; + // Set empty command in order to clear the candidates which have been registered into the next + // view. Otherwise such candidates (typically they are obsolete) are shown unexpectedly. + // TODO(hsumita): Revisit when Mozc server returns candidates for SWITCH_INPUT_MODE command. + updateWithoutAnimation(EMPTY_COMMAND); + } + + public void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) { + this.candidateTextSize = candidateTextSize; + this.descriptionTextSize = descriptionTextSize; + if (numberCandidateView.isPresent()) { + numberCandidateView.get().setCandidateTextDimension( + candidateTextSize, descriptionTextSize); + } else { + keyboardCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize); + } + } + + public void setInputFrameFoldButtonChecked(boolean isChecked) { + switch (candidateMode) { + case KEYBOARD: + keyboardCandidateView.setInputFrameFoldButtonChecked(isChecked); + break; + case NUMBER: + numberCandidateView.get().setInputFrameFoldButtonChecked(isChecked); + break; + case FLOATING: + throw new IllegalStateException("Fold button is not available on floating mode."); + } + } + + public void setEditorInfo(EditorInfo info) { + this.editorInfo = Preconditions.checkNotNull(info); + if (candidateMode == CandidateMode.FLOATING) { + floatingCandidateView.setEditorInfo(info); + } + } + + @TargetApi(21) + public void setCursorAnchorInfo(CursorAnchorInfo info) { + this.cursorAnchorInfo = Preconditions.checkNotNull(info); + if (candidateMode == CandidateMode.FLOATING) { + floatingCandidateView.setCursorAnchorInfo(info); + } + } + + public void setSkin(Skin skin) { + this.skin = Preconditions.checkNotNull(skin); + keyboardCandidateView.setSkin(skin); + if (numberCandidateView.isPresent()) { + numberCandidateView.get().setSkin(skin); + } + } + + public void setEventListener(ViewEventListener viewEventListener, + KeyboardCandidateViewHeightListener hightListener) { + this.viewEventListener = Optional.of(viewEventListener); + this.keyboardCandidateViewHeightListener = Optional.of(hightListener); + keyboardCandidateView.setViewEventListener(viewEventListener); + floatingCandidateView.setViewEventListener(viewEventListener); + if (numberCandidateView.isPresent()) { + numberCandidateView.get().setViewEventListener(viewEventListener); + } + } + + /** + * Set true if extracted mode (== fullscreen mode) is activated. + */ + public void setExtractedMode(boolean isExtractedMode) { + this.isExtractedMode = isExtractedMode; + updateCandiadateWindowActivation(); + } + + public void setHardwareCompositionMode(CompositionMode mode) { + if (isFloatingMode()) { + floatingCandidateView.setCompositionMode(mode); + } + } + + public void reset() { + keyboardCandidateView.clearAnimation(); + keyboardCandidateView.setVisibility(View.GONE); + keyboardCandidateView.reset(); + if (numberCandidateView.isPresent()) { + numberCandidateView.get().clearAnimation(); + numberCandidateView.get().setVisibility(View.GONE); + numberCandidateView.get().reset(); + } + + candidateMode = CandidateMode.KEYBOARD; + floatingCandidateView.setVisibility(View.GONE); + } + + public void resetHeightDependingComponents( + Resources resources, int windowHeight, int inputFrameHeight) { + Preconditions.checkNotNull(resources); + + int keyboardCandidateViewHeight = windowHeight - inputFrameHeight; + long duration = resources.getInteger(R.integer.candidate_frame_transition_duration); + float fromAlpha = 0.0f; + float toAlpha = 1.0f; + + keyboardCandidateView.setInAnimation(createKeyboardCandidateViewTransitionAnimation( + keyboardCandidateViewHeight, 0, fromAlpha, toAlpha, duration)); + keyboardCandidateView.setOutAnimation(createKeyboardCandidateViewTransitionAnimation( + 0, keyboardCandidateViewHeight, toAlpha, fromAlpha, duration)); + + int numberCandidateViewHeight = resources.getDimensionPixelSize(R.dimen.button_frame_height); + numberCandidateViewInAnimation = createKeyboardCandidateViewTransitionAnimation( + numberCandidateViewHeight, 0, fromAlpha, toAlpha, duration); + numberCandidateViewOutAnimation = createKeyboardCandidateViewTransitionAnimation( + 0, numberCandidateViewHeight, toAlpha, fromAlpha, duration); + if (numberCandidateView.isPresent()) { + numberCandidateView.get().setInAnimation(numberCandidateViewInAnimation); + numberCandidateView.get().setOutAnimation(numberCandidateViewOutAnimation); + } + } + + public boolean isKeyboardCandidateViewVisible() { + return keyboardCandidateView.getVisibility() == View.VISIBLE; + } + + private static Animation createKeyboardCandidateViewTransitionAnimation( + int fromY, int toY, float fromAlpha, float toAlpha, long duration) { + AnimationSet animation = new AnimationSet(false); + animation.setDuration(duration); + + AlphaAnimation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha); + alphaAnimation.setDuration(duration); + animation.addAnimation(alphaAnimation); + + TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, fromY, toY); + translateAnimation.setInterpolator(new DecelerateInterpolator()); + translateAnimation.setDuration(duration); + animation.addAnimation(translateAnimation); + return animation; + } + + private void startKeyboardCandidateViewInAnimation() { + switch (candidateMode) { + case KEYBOARD: + keyboardCandidateView.startInAnimation(); + if (keyboardCandidateViewHeightListener.isPresent()) { + keyboardCandidateViewHeightListener.get().onExpanded(); + } + break; + case NUMBER: + numberCandidateView.get().startInAnimation(); + break; + case FLOATING: + throw new IllegalStateException("Floating mode doesn't support in-animation."); + } + } + + private void startKeyboardCandidateViewOutAnimation() { + switch (candidateMode) { + case KEYBOARD: + if (keyboardCandidateViewHeightListener.isPresent()) { + keyboardCandidateViewHeightListener.get().onCollapse(); + } + keyboardCandidateView.startOutAnimation(); + break; + case NUMBER: + numberCandidateView.get().startOutAnimation(); + break; + case FLOATING: + throw new IllegalStateException("Floating mode doesn't support out-animation."); + } + } + + private static boolean hasCandidates(Command command) { + return command.getOutput().getAllCandidateWords().getCandidatesCount() > 0; + } + + @Override + public void trimMemory() { + keyboardCandidateView.trimMemory(); + if (numberCandidateView.isPresent()) { + numberCandidateView.get().trimMemory(); + } + } + + public boolean isFloatingMode() { + return candidateMode == CandidateMode.FLOATING; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java b/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java index d456b9948..f9ff8beb0 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java @@ -42,23 +42,23 @@ import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer; import org.mozc.android.inputmethod.japanese.ui.CandidateLayouter; import org.mozc.android.inputmethod.japanese.ui.SnapScroller; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.CarrierEmojiRenderHelper; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Paint.Align; import android.graphics.RectF; +import android.graphics.drawable.Drawable; import android.support.v4.view.ViewCompat; -import android.support.v4.widget.EdgeEffectCompat; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; +import android.widget.EdgeEffect; import javax.annotation.Nullable; @@ -73,14 +73,16 @@ abstract class CandidateWordView extends View implements MemoryManageable { * Handles gestures to scroll candidate list and choose a candidate. */ class CandidateWordGestureDetector { + class CandidateWordViewGestureListener extends SimpleOnGestureListener { + @Override public boolean onFling( MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { float velocity = orientationTrait.projectVector(velocityX, velocityY); // As fling is started, current action is not tapping. // Reset pressing state so that candidate selection is not triggered at touch up event. - releaseCandidate(); + reset(); // Fling makes scrolling. scroller.fling(-(int) velocity); invalidate(); @@ -96,7 +98,7 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d orientationTrait.scrollTo(CandidateWordView.this, scroller.getScrollPosition()); // As scroll is started, current action is not tapping. // Reset pressing state so that candidate selection is not triggered at touch up event. - releaseCandidate(); + reset(); // Edge effect. Now, in production, we only support vertical scroll. if (oldScrollPosition + distance < 0) { @@ -119,7 +121,7 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d // GestureDetector cannot handle all complex gestures which we need. // But we use GestureDetector for some gesture recognition // because implementing whole gesture detection logic by ourselves is a bit tedious. - final GestureDetector gestureDetector; + private final GestureDetector gestureDetector; /** * Points to an instance of currently pressed candidate word. Or {@code null} if any @@ -146,9 +148,10 @@ private void pressCandidate(int rowIndex, Span span) { span.getRight(), row.getTop() + row.getHeight()); } - private void releaseCandidate() { + void reset() { pressedCandidate = null; pressedRowIndex = Optional.absent(); + // NOTE: candidateRect doesn't need reset. } CandidateWord getPressedCandidate() { @@ -195,6 +198,14 @@ private boolean findCandidateAndPress(float scrolledX, float scrolledY) { } boolean onTouchEvent(MotionEvent event) { + // Before delegation to gesture detector, handle ACTION_UP event + // in order to release edge effect. + if (event.getAction() == MotionEvent.ACTION_UP) { + topEdgeEffect.onRelease(); + bottomEdgeEffect.onRelease(); + invalidate(); + } + if (gestureDetector.onTouchEvent(event)) { return true; } @@ -207,23 +218,25 @@ boolean onTouchEvent(MotionEvent event) { scroller.stopScrolling(); if (!topEdgeEffect.isFinished()) { topEdgeEffect.onRelease(); + invalidate(); } if (!bottomEdgeEffect.isFinished()) { bottomEdgeEffect.onRelease(); + invalidate(); } return true; case MotionEvent.ACTION_MOVE: if (pressedCandidate != null) { // Turn off highlighting if contact point gets out of the candidate. if (!candidateRect.contains(scrolledX, scrolledY)) { - releaseCandidate(); + reset(); invalidate(); } } return true; case MotionEvent.ACTION_CANCEL: if (pressedCandidate != null) { - releaseCandidate(); + reset(); invalidate(); } return true; @@ -232,7 +245,7 @@ boolean onTouchEvent(MotionEvent event) { if (candidateRect.contains(scrolledX, scrolledY) && candidateSelectListener != null) { candidateSelectListener.onCandidateSelected(pressedCandidate, pressedRowIndex); } - releaseCandidate(); + reset(); invalidate(); } return true; @@ -346,8 +359,8 @@ public float getContentSize(Optional layout) { // Finally, we only need vertical scrolling. // TODO(hidehiko): Remove horizontal scrolling related codes. - private final EdgeEffectCompat topEdgeEffect = new EdgeEffectCompat(getContext()); - private final EdgeEffectCompat bottomEdgeEffect = new EdgeEffectCompat(getContext()); + private final EdgeEffect topEdgeEffect = new EdgeEffect(getContext()); + private final EdgeEffect bottomEdgeEffect = new EdgeEffect(getContext()); // The Scroller which manages the status of scrolling the view. // Default behavior of ScrollView does not suffice our UX design @@ -367,8 +380,10 @@ public float getContentSize(Optional layout) { // No padding by default. private int horizontalPadding = 0; + protected final CarrierEmojiRenderHelper carrierEmojiRenderHelper = + new CarrierEmojiRenderHelper(this); protected final CandidateLayoutRenderer candidateLayoutRenderer = - new CandidateLayoutRenderer(this); + new CandidateLayoutRenderer(); CandidateWordGestureDetector candidateWordGestureDetector = new CandidateWordGestureDetector(getContext()); @@ -377,7 +392,7 @@ public float getContentSize(Optional layout) { private final OrientationTrait orientationTrait; protected final BackgroundDrawableFactory backgroundDrawableFactory = - new BackgroundDrawableFactory(getResources().getDisplayMetrics().density); + new BackgroundDrawableFactory(getResources()); private DrawableType backgroundDrawableType = null; private final CandidateWindowAccessibilityDelegate accessibilityDelegate; @@ -404,6 +419,12 @@ public float getContentSize(Optional layout) { ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate); } + void reset() { + calculatedLayout = null; + currentCandidateList = null; + candidateWordGestureDetector.reset(); + } + void setCandidateSelectListener(CandidateSelectListener candidateSelectListener) { this.candidateSelectListener = candidateSelectListener; } @@ -436,19 +457,18 @@ public boolean onTouchEvent(MotionEvent event) { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - candidateLayoutRenderer.onAttachedToWindow(); + carrierEmojiRenderHelper.onAttachedToWindow(); } @Override protected void onDetachedFromWindow() { - candidateLayoutRenderer.onDetachedFromWindow(); + carrierEmojiRenderHelper.onDetachedFromWindow(); super.onDetachedFromWindow(); } public void setEmojiProviderType(EmojiProviderType providerType) { Preconditions.checkNotNull(providerType); - - candidateLayoutRenderer.setEmojiProviderType(providerType); + carrierEmojiRenderHelper.setEmojiProviderType(providerType); } @Override @@ -506,7 +526,8 @@ protected void onDraw(Canvas canvas) { CandidateWord pressedCandidate = candidateWordGestureDetector.getPressedCandidate(); int pressedCandidateIndex = (pressedCandidate != null && pressedCandidate.hasIndex()) ? pressedCandidate.getIndex() : -1; - candidateLayoutRenderer.drawCandidateLayout(canvas, calculatedLayout, pressedCandidateIndex); + candidateLayoutRenderer.drawCandidateLayout( + canvas, calculatedLayout, pressedCandidateIndex, carrierEmojiRenderHelper); } finally { canvas.restoreToCount(saveCount); } @@ -525,11 +546,13 @@ public final void computeScroll() { topEdgeEffect.onAbsorb(velocity.intValue()); if (!bottomEdgeEffect.isFinished()) { bottomEdgeEffect.onRelease(); + invalidate(); } } else if (velocity > 0) { bottomEdgeEffect.onAbsorb(velocity.intValue()); if (!topEdgeEffect.isFinished()) { topEdgeEffect.onRelease(); + invalidate(); } } } @@ -586,7 +609,9 @@ void setScrollPosition(int position) { void update(CandidateList candidateList) { CandidateList previousCandidateList = currentCandidateList; currentCandidateList = candidateList; - candidateLayoutRenderer.setCandidateList(Optional.fromNullable(candidateList)); + Optional optionalCandidateList = Optional.fromNullable(candidateList); + candidateLayoutRenderer.setCandidateList(optionalCandidateList); + carrierEmojiRenderHelper.setCandidateList(optionalCandidateList); if (layouter != null && !equals(candidateList, previousCandidateList)) { updateCalculatedLayout(); } @@ -655,32 +680,29 @@ public CandidateList getCandidateList() { return currentCandidateList; } - /** - * Utility method for creating paint instance. - */ - protected static Paint createPaint( - boolean antiAlias, int color, Align textAlign, float textSize) { - Paint paint = new Paint(); - paint.setAntiAlias(antiAlias); - paint.setColor(color); - paint.setTextAlign(textAlign); - paint.setTextSize(textSize); - return paint; - } - - protected void setBackgroundDrawableType(DrawableType drawableType) { + protected void setSpanBackgroundDrawableType(DrawableType drawableType) { backgroundDrawableType = drawableType; - resetBackground(); + resetSpanBackground(); } - private void resetBackground() { - candidateLayoutRenderer.setSpanBackgroundDrawable( - Optional.fromNullable(backgroundDrawableFactory.getDrawable(backgroundDrawableType))); + private void resetSpanBackground() { + Drawable drawable = (backgroundDrawableType != null) + ? backgroundDrawableFactory.getDrawable(backgroundDrawableType) : null; + candidateLayoutRenderer.setSpanBackgroundDrawable(Optional.fromNullable(drawable)); } - void setSkinType(SkinType skinType) { - backgroundDrawableFactory.setSkinType(skinType); - resetBackground(); + /** + * Returns a Drawable which should be set as the view's background. + */ + protected abstract Drawable getViewBackgroundDrawable(Skin skin); + + @SuppressWarnings("deprecation") + void setSkin(Skin skin) { + backgroundDrawableFactory.setSkin(Preconditions.checkNotNull(skin)); + resetSpanBackground(); + candidateLayoutRenderer.setSkin(skin); + setBackgroundDrawable( + getViewBackgroundDrawable(skin).getConstantState().newDrawable()); } @Override @@ -697,4 +719,15 @@ protected boolean dispatchHoverEvent(MotionEvent event) { } return false; } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + // If this view gets invisible, reset the internal state of the gesture detector. + // Otherwise UP event, which is sent after this view being invisible, will cause + // unexpected onCandidateSelected callback. + if (visibility != View.VISIBLE) { + candidateWordGestureDetector.reset(); + } + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java b/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java index 532574564..4a1e04d96 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java @@ -194,4 +194,9 @@ public static ComposingTextTrackingInputConnection newInstance(InputConnection b } return new ComposingTextTrackingInputConnection(baseConnection); } + + @Override + public boolean requestCursorUpdates(int cursorUpdateMode) { + return baseConnection.requestCursorUpdates(cursorUpdateMode); + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java b/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java index 1c88a7499..6507095b2 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java @@ -78,8 +78,7 @@ public ConversionCandidateWordContainerView(Context context) { void setCandidateTextDimension(float candidateTextSize) { Preconditions.checkArgument(candidateTextSize > 0); - foldingIconSize = candidateTextSize - + getResources().getDimension(R.dimen.candidate_vertical_padding_size) * 2; + foldingIconSize = getResources().getDimension(R.dimen.candidate_fold_icon_width); } @Override diff --git a/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java b/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java index 8880954db..38c25e4b6 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java @@ -52,6 +52,7 @@ */ public class DependencyFactory { + /** * Dependencies. */ diff --git a/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java b/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java index 50d5ff0c1..98a76e4c9 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java +++ b/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java @@ -53,11 +53,35 @@ public enum FeedbackEvent { /** * Fired when the input view is expanded (the candidate view is fold). */ - INPUTVIEW_EXPAND(true), + INPUTVIEW_EXPAND(true, AudioManager.FX_KEYPRESS_STANDARD), /** * Fired when the input view is fold (the candidate view is expand). */ - INPUTVIEW_FOLD(true), + INPUTVIEW_FOLD(true, AudioManager.FX_KEYPRESS_STANDARD), + /** + * Fired when the symbol input view is closed. + */ + SYMBOL_INPUTVIEW_CLOSED(true, AudioManager.FX_KEYPRESS_STANDARD), + /** + * Fired when a minor category is selected. + */ + SYMBOL_INPUTVIEW_MINOR_CATEGORY_SELECTED(true, AudioManager.FX_KEYPRESS_STANDARD), + /** + * Fired when a major category is selected. + */ + SYMBOL_INPUTVIEW_MAJOR_CATEGORY_SELECTED(true, AudioManager.FX_KEYPRESS_STANDARD), + /** + * Fired when microphone button is touched. + */ + MICROPHONE_BUTTON_DOWN(true, AudioManager.FX_KEYPRESS_STANDARD), + /** + * Fired when the hardware composition button in narrow frame is touched. + */ + NARROW_FRAME_HARDWARE_COMPOSITION_BUTTON_DOWN(true, AudioManager.FX_KEYPRESS_STANDARD), + /** + * Fired when the widen button in narrow frame is touched. + */ + NARROW_FRAME_WIDEN_BUTTON_DOWN(true, AudioManager.FX_KEYPRESS_STANDARD), ; // Constant value to indicate no sound feedback should be played. static final int NO_SOUND = -1; @@ -103,7 +127,7 @@ interface FeedbackListener { private boolean isHapticFeedbackEnabled; private long hapticFeedbackDuration = 30; // 30ms by default. private boolean isSoundFeedbackEnabled; - private float soundFeedbackVolume = 0.1f; // System default volume parameter. + private float soundFeedbackVolume = 0.4f; // System default volume parameter. @VisibleForTesting final FeedbackListener feedbackListener; /** diff --git a/src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java b/src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java new file mode 100644 index 000000000..97048db71 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java @@ -0,0 +1,550 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese; + +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.Category; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment; +import org.mozc.android.inputmethod.japanese.ui.FloatingCandidateLayoutRenderer; +import org.mozc.android.inputmethod.japanese.ui.FloatingModeIndicator; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorInfo; +import android.widget.PopupWindow; + +/** + * Floating candidate view for hardware keyboard. + */ +@TargetApi(21) +public class FloatingCandidateView extends View { + + private interface FloatingCandidateViewProxy { + public void draw(Canvas canvas); + public void viewSizeChanged(int width, int height); + public void setCursorAnchorInfo(CursorAnchorInfo info); + public void setCandidates(Command outCommand); + public void setEditorInfo(EditorInfo editorInfo); + public void setCompositionMode(CompositionMode mode); + public void setViewEventListener(ViewEventListener listener); + public void setVisibility(int visibility); + public Optional getVisibleRect(); + } + + private static class FloatingCandidateViewStub implements FloatingCandidateViewProxy { + @Override + public void draw(Canvas canvas) {} + + @Override + public void viewSizeChanged(int width, int height) {} + + @Override + public void setCursorAnchorInfo(CursorAnchorInfo info) {} + + @Override + public void setCandidates(Command outCommand) {} + + @Override + public void setEditorInfo(EditorInfo editorInfo) {} + + @Override + public void setCompositionMode(CompositionMode mode) {} + + @Override + public void setViewEventListener(ViewEventListener listener) {} + + @Override + public void setVisibility(int visibility) {} + + @Override + public Optional getVisibleRect() { + return Optional.absent(); + } + } + + @TargetApi(21) + private static class FloatingCandidateViewImpl implements FloatingCandidateViewProxy { + + private final View parentView; + + /** Layouts the floating candidate window and draws it's contents. */ + private final FloatingCandidateLayoutRenderer layoutRenderer; + + private final FloatingModeIndicator modeIndicator; + + /** + * Pop-up window to handle touch events. + *

+ * A touch down event on outside a touchable region (set by {@link MozcService#onComputeInsets}) + * cannot be caught by view, and we cannot expand the touchable region since all touch down + * events inside the region are not delegated to a background application. + * To handle these touch events, we employ pop-up window. + *

+ * This window is always invisible since we cannot control the transition behavior. + * (e.g. Pop-up window always move with animation) + */ + private final PopupWindow touchEventReceiverWindow; + + private final int windowVerticalMargin; + private final int windowHorizontalMargin; + + /** + * Base position of the floating candidate window. + *

+ * It is same as the cursor rectangle on pre-composition state, and the left-edge of the focused + * segment on other states. + */ + private int basePositionTop; + private int basePositionBottom; + private int basePositionX; + private Optional cursorAnchorInfo = Optional.absent(); + private Category candidatesCategory = Category.CONVERSION; + private int highlightedCharacterStart; + private int compositionCharacterEnd; + /** True if EditorInfo says suggestion should be suppressed. */ + private boolean suppressSuggestion; + /** + * Horizontal offset of the candidate window. See also {@link FloatingCandidateLayoutRenderer} + */ + private int offsetX; + /** Vertical offset of the candidate window. See also {@link FloatingCandidateLayoutRenderer} */ + private int offsetY; + private boolean isCandidateWindowShowing; + + public FloatingCandidateViewImpl(View parentView) { + Context context = Preconditions.checkNotNull(parentView).getContext(); + this.parentView = parentView; + this.layoutRenderer = new FloatingCandidateLayoutRenderer(context.getResources()); + this.modeIndicator = new FloatingModeIndicator(parentView); + this.touchEventReceiverWindow = createPopupWindow(context); + Resources resources = context.getResources(); + this.windowVerticalMargin = + Math.round(resources.getDimension(R.dimen.floating_candidate_window_vertical_margin)); + this.windowHorizontalMargin = + Math.round(resources.getDimension(R.dimen.floating_candidate_window_horizontal_margin)); + } + + public FloatingCandidateViewImpl(View parentView, PopupWindow popupWindowMock, + FloatingCandidateLayoutRenderer layoutRenderer, + FloatingModeIndicator modeIndicator) { + this.parentView = Preconditions.checkNotNull(parentView); + this.layoutRenderer = Preconditions.checkNotNull(layoutRenderer); + this.modeIndicator = Preconditions.checkNotNull(modeIndicator); + this.touchEventReceiverWindow = Preconditions.checkNotNull(popupWindowMock); + Resources resources = parentView.getContext().getResources(); + this.windowVerticalMargin = + resources.getDimensionPixelSize(R.dimen.floating_candidate_window_vertical_margin); + this.windowHorizontalMargin = + resources.getDimensionPixelSize(R.dimen.floating_candidate_window_horizontal_margin); + } + + private PopupWindow createPopupWindow(Context context) { + return new PopupWindow(new View(context) { + @Override + public boolean onTouchEvent(MotionEvent event) { + Optional rect = layoutRenderer.getWindowRect(); + if (!rect.isPresent()) { + return false; + } + + MotionEvent copiedEvent = MotionEvent.obtain(event); + try { + copiedEvent.offsetLocation(rect.get().left, rect.get().top); + layoutRenderer.onTouchEvent(copiedEvent); + // TODO(hsumita): Don't invalidate the view if not necessary. + parentView.invalidate(); + } finally { + copiedEvent.recycle(); + } + return true; + } + }); + } + + @Override + public void draw(Canvas canvas) { + if (!isCandidateWindowShowing) { + return; + } + + int saveId = canvas.save(Canvas.MATRIX_SAVE_FLAG); + try { + canvas.translate(offsetX, offsetY); + layoutRenderer.draw(canvas); + } finally { + canvas.restoreToCount(saveId); + } + } + + @Override + public void viewSizeChanged(int width, int height) { + layoutRenderer.setMaxWidth(width - windowHorizontalMargin * 2); + updateCandidateWindowWithSize(width, height); + } + + /** Sets {@link CursorAnchorInfo} to update the candidate window position. */ + @Override + public void setCursorAnchorInfo(CursorAnchorInfo info) { + cursorAnchorInfo = Optional.of(info); + modeIndicator.setCursorAnchorInfo(info); + updateCandidateWindow(); + } + + /** Sets {@link Command} to update the contents of the candidate window. */ + @Override + public void setCandidates(Command outCommand) { + Output output = Preconditions.checkNotNull(outCommand).getOutput(); + layoutRenderer.setCandidates(outCommand); + modeIndicator.setCommand(outCommand); + highlightedCharacterStart = output.getPreedit().getHighlightedPosition(); + int currentPreeditPosition = 0; + for (Segment segment : output.getPreedit().getSegmentList()) { + currentPreeditPosition += segment.getValueLength(); + } + compositionCharacterEnd = currentPreeditPosition; + candidatesCategory = output.getCandidates().getCategory(); + updateCandidateWindow(); + } + + /** Sets {@link EditorInfo} for context-aware behavior. */ + @Override + public void setEditorInfo(EditorInfo editorInfo) { + Preconditions.checkNotNull(editorInfo); + boolean previusSuppressSuggestion = suppressSuggestion; + suppressSuggestion = shouldSuppressSuggestion(editorInfo); + if (previusSuppressSuggestion != suppressSuggestion) { + updateCandidateWindow(); + } + } + + @Override + public void setCompositionMode(CompositionMode mode) { + modeIndicator.setCompositionMode(mode); + } + + /** Set view event listener to handle events invoked by the candidate window. */ + @Override + public void setViewEventListener(ViewEventListener listener) { + layoutRenderer.setViewEventListener(Preconditions.checkNotNull(listener)); + } + + @Override + public void setVisibility(int visibility) { + if (visibility != View.VISIBLE) { + modeIndicator.hide(); + } + } + + /** + * Updates the candidate window. + *

+ * All layout related states should be updated before call this method. + */ + private void updateCandidateWindow() { + updateCandidateWindowWithSize(parentView.getWidth(), parentView.getHeight()); + } + + private int calculateWindowLeftPosition(Rect rect, int basePositionX, int viewWidth) { + return MozcUtil.clamp( + basePositionX + rect.left, + windowHorizontalMargin, viewWidth - rect.width() - windowHorizontalMargin); + } + + /** + * Updates the candidate window with width and height. + *

+ * All layout related states should be updated before call this method. + */ + private void updateCandidateWindowWithSize(int viewWidth, int viewHeight) { + if (suppressSuggestion && candidatesCategory == Category.SUGGESTION) { + dismissCandidateWindow(); + return; + } + + Optional optionalWindowRect = layoutRenderer.getWindowRect(); + if (!optionalWindowRect.isPresent()) { + dismissCandidateWindow(); + return; + } + + Rect rect = optionalWindowRect.get(); + updateBasePosition(rect, viewWidth); + int lowerAreaHeight = viewHeight - basePositionBottom - windowVerticalMargin; + int upperAreaHeight = basePositionTop - windowVerticalMargin; + int top = (lowerAreaHeight < rect.height() && lowerAreaHeight < upperAreaHeight) + ? MozcUtil.clamp(basePositionTop - rect.height() - windowVerticalMargin, + 0, viewHeight - rect.height()) + : Math.max(0, basePositionBottom + windowVerticalMargin); + int left = calculateWindowLeftPosition(rect, basePositionX, viewWidth); + + offsetX = left - rect.left; + offsetY = top - rect.top; + + rect.offset(offsetX, offsetY); + showCandidateWindow(rect); + } + + /** Return true if floating candidate window should be suppressed. */ + private boolean shouldSuppressSuggestion(EditorInfo editorInfo) { + if ((editorInfo.inputType & EditorInfo.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) { + return true; + } + + if ((editorInfo.inputType + & (InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)) + != 0) { + return true; + } + + switch (editorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION) { + case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_URI: + case InputType.TYPE_TEXT_VARIATION_FILTER: + return true; + default: + return false; + } + } + + private void resetBasePosition() { + basePositionTop = 0; + basePositionBottom = 0; + basePositionX = 0; + return; + } + + /** + * Update {@code basePositionTop}, {@code basePositionBottom} and {@code basePositionX} using + * {@code cursorAnchorInfo}. + */ + private void updateBasePosition(Rect windowRect, int viewWidth) { + if (!cursorAnchorInfo.isPresent()) { + resetBasePosition(); + return; + } + + CursorAnchorInfo info = cursorAnchorInfo.get(); + int composingStartIndex = info.getComposingTextStart() + highlightedCharacterStart; + int composingEndIndex = info.getComposingTextStart() + compositionCharacterEnd - 1; + RectF firstCharacterBounds = info.getCharacterBounds(composingStartIndex); + float[] points; + if (firstCharacterBounds != null) { + points = new float[] {firstCharacterBounds.left, firstCharacterBounds.top, + firstCharacterBounds.left, firstCharacterBounds.bottom}; + } else if (!Float.isNaN(info.getInsertionMarkerHorizontal())) { + points = new float[] {info.getInsertionMarkerHorizontal(), info.getInsertionMarkerTop(), + info.getInsertionMarkerHorizontal(), info.getInsertionMarkerBottom()}; + } else { + resetBasePosition(); + return; + } + + // Adjust the bottom base position not to hide composition characters by the floating + // candidate window. + int windowLeft = calculateWindowLeftPosition(windowRect, (int) points[0], viewWidth); + for (int i = composingEndIndex; i > composingStartIndex; --i) { + RectF bounds = info.getCharacterBounds(i); + if (bounds == null) { + continue; + } + if (bounds.bottom <= points[3]) { + break; + } + if (bounds.right > windowLeft) { + points[3] = bounds.bottom; + break; + } + } + + info.getMatrix().mapPoints(points); + int[] screenOffset = new int[2]; + parentView.getLocationOnScreen(screenOffset); + basePositionX = Math.round(points[0]) - screenOffset[0]; + basePositionTop = Math.round(points[1]) - screenOffset[1]; + basePositionBottom = Math.round(points[3]) - screenOffset[1]; + } + + /** + * Shows the candidate window. + *

+ * First {@code touchEventReceiverWindow} is shown (or is updated its position if it has been + * already shown). Then this view is invalidated. As the result {@code draw} will be called back + * and visible candidate window will be shown. + */ + private void showCandidateWindow(Rect rect) { + isCandidateWindowShowing = true; + if (touchEventReceiverWindow.isShowing()) { + touchEventReceiverWindow.update(rect.left, rect.top, rect.width(), rect.height()); + } else { + touchEventReceiverWindow.setWidth(rect.width()); + touchEventReceiverWindow.setHeight(rect.height()); + touchEventReceiverWindow.showAtLocation( + parentView, Gravity.NO_GRAVITY, rect.left, rect.top); + } + parentView.postInvalidate(); + } + + /** + * Dismisses the candidate window. + *

+ * Does the very similar things as {@showCandidateWindow}. + */ + private void dismissCandidateWindow() { + if (isCandidateWindowShowing) { + isCandidateWindowShowing = false; + touchEventReceiverWindow.dismiss(); + parentView.postInvalidate(); + } + } + + @Override + public Optional getVisibleRect() { + Optional rect = layoutRenderer.getWindowRect(); + if (touchEventReceiverWindow.isShowing() && rect.isPresent()) { + rect.get().offset(offsetX, offsetY); + return rect; + } else { + return Optional.absent(); + } + } + } + + private final FloatingCandidateViewProxy floatingCandidateViewProxy; + + public FloatingCandidateView(Context context) { + super(context); + floatingCandidateViewProxy = createFloatingCandidateViewInstance(this); + } + + public FloatingCandidateView(Context context, AttributeSet attrs) { + super(context, attrs); + floatingCandidateViewProxy = createFloatingCandidateViewInstance(this); + } + + @VisibleForTesting + FloatingCandidateView(Context context, PopupWindow popupWindowMock) { + super(context); + floatingCandidateViewProxy = new FloatingCandidateViewImpl( + this, popupWindowMock, new FloatingCandidateLayoutRenderer(context.getResources()), + new FloatingModeIndicator(this)); + } + + @VisibleForTesting + FloatingCandidateView(Context context, PopupWindow popupWindowMock, + FloatingCandidateLayoutRenderer layoutRenderer, + FloatingModeIndicator modeIndicator) { + super(context); + floatingCandidateViewProxy = + new FloatingCandidateViewImpl(this, popupWindowMock, layoutRenderer, modeIndicator); + } + + private static FloatingCandidateViewProxy createFloatingCandidateViewInstance(View view) { + return isAvailable() + ? new FloatingCandidateViewImpl(view) + : new FloatingCandidateViewStub(); + } + + @Override + protected void onFinishInflate() { + // Use software renderer since hardware renderer doesn't support Paint#setShadowLayer() and + // Canvas#drawPicture() which is used by MozcDrawable. + this.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + public static boolean isAvailable() { + return Build.VERSION.SDK_INT >= 21; + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + floatingCandidateViewProxy.setVisibility(visibility); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + floatingCandidateViewProxy.draw(canvas); + } + + @Override + public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + floatingCandidateViewProxy.viewSizeChanged(width, height); + } + + /** Sets {@link CursorAnchorInfo} to update the candidate window position. */ + public void setCursorAnchorInfo(CursorAnchorInfo info) { + floatingCandidateViewProxy.setCursorAnchorInfo(info); + } + + /** Sets {@link Command} to update the contents of the candidate window. */ + public void setCandidates(Command outCommand) { + floatingCandidateViewProxy.setCandidates(outCommand); + } + + /** Sets {@link EditorInfo} for context-aware behavior. */ + public void setEditorInfo(EditorInfo editorInfo) { + floatingCandidateViewProxy.setEditorInfo(editorInfo); + } + + public void setCompositionMode(CompositionMode mode) { + floatingCandidateViewProxy.setCompositionMode(mode); + } + + /** Set view event listener to handle events invoked by the candidate window. */ + public void setViewEventListener(ViewEventListener listener) { + floatingCandidateViewProxy.setViewEventListener(listener); + } + + @VisibleForTesting Optional getVisibleRect() { + return floatingCandidateViewProxy.getVisibleRect(); + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java b/src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java new file mode 100644 index 000000000..847692c7b --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java @@ -0,0 +1,39 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +class InputDeviceReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) {} +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java b/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java deleted file mode 100644 index 7b2ec9d20..000000000 --- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2010-2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package org.mozc.android.inputmethod.japanese; - -import org.mozc.android.inputmethod.japanese.keyboard.Flick; -import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction; -import org.mozc.android.inputmethod.japanese.keyboard.Key; -import org.mozc.android.inputmethod.japanese.keyboard.KeyEntity; -import org.mozc.android.inputmethod.japanese.keyboard.KeyState; -import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; -import org.mozc.android.inputmethod.japanese.keyboard.Row; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable; -import org.mozc.android.inputmethod.japanese.resources.R; -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; - -import android.util.SparseIntArray; - -import java.util.List; - -/** - */ -public class JapaneseKeyboard extends Keyboard { - /** - * Each keyboard has its own specification. - * - * For example, some keyboards use a special Romanji table. - */ - public static enum KeyboardSpecification { - // 12 keys. - TWELVE_KEY_TOGGLE_KANA( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_KANA", 0, 1, 1), - R.xml.kbd_12keys_kana, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.TWELVE_KEYS_TO_HIRAGANA, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - true, - CrossingEdgeBehavior.DO_NOTHING), - - TWELVE_KEY_TOGGLE_ALPHABET( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_ALPHABET", 0, 1, 1), - R.xml.kbd_12keys_abc, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.TWELVE_KEYS_TO_HALFWIDTHASCII, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.DO_NOTHING), - - TWELVE_KEY_TOGGLE_NUMBER( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_NUMBER", 0, 1, 1), - R.xml.kbd_12keys_123, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.TWELVE_KEYS_TO_NUMBER, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.DO_NOTHING), - - TWELVE_KEY_TOGGLE_QWERTY_ALPHABET( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_QWERTY_ALPHABET", 0, 4, 0), - R.xml.kbd_12keys_qwerty_abc, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), - - // Flick mode. - TWELVE_KEY_FLICK_KANA( - new KeyboardSpecificationName("TWELVE_KEY_FLICK_KANA", 0, 1, 3), - R.xml.kbd_12keys_flick_kana, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.FLICK_TO_HIRAGANA, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - true, - CrossingEdgeBehavior.DO_NOTHING), - - TWELVE_KEY_FLICK_ALPHABET( - new KeyboardSpecificationName("TWELVE_KEY_FLICK_ALPHABET", 0, 1, 1), - R.xml.kbd_12keys_flick_abc, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.FLICK_TO_HALFWIDTHASCII, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), - - TWELVE_KEY_FLICK_NUMBER( - new KeyboardSpecificationName("TWELVE_KEY_FLICK_NUMBER", 0, 1, 1), - R.xml.kbd_12keys_flick_123, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.FLICK_TO_NUMBER, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), - - TWELVE_KEY_TOGGLE_FLICK_KANA( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_KANA", 0, 1, 3), - R.xml.kbd_12keys_flick_kana, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.TOGGLE_FLICK_TO_HIRAGANA, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - true, - CrossingEdgeBehavior.DO_NOTHING), - - TWELVE_KEY_TOGGLE_FLICK_ALPHABET( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 1, 1), - R.xml.kbd_12keys_flick_abc, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.TOGGLE_FLICK_TO_HALFWIDTHASCII, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.DO_NOTHING), - - TWELVE_KEY_TOGGLE_FLICK_NUMBER( - new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 1, 1), - R.xml.kbd_12keys_flick_123, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.TOGGLE_FLICK_TO_NUMBER, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.DO_NOTHING), - - // QWERTY keyboard. - QWERTY_KANA( - new KeyboardSpecificationName("QWERTY_KANA", 0, 3, 1), - R.xml.kbd_qwerty_kana, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - false, - CrossingEdgeBehavior.DO_NOTHING), - - QWERTY_KANA_NUMBER( - new KeyboardSpecificationName("QWERTY_KANA_NUMBER", 0, 2, 1), - R.xml.kbd_qwerty_kana_123, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA_NUMBER, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - false, - CrossingEdgeBehavior.DO_NOTHING), - - QWERTY_ALPHABET( - new KeyboardSpecificationName("QWERTY_ALPHABET", 0, 4, 0), - R.xml.kbd_qwerty_abc, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), - - QWERTY_ALPHABET_NUMBER( - new KeyboardSpecificationName("QWERTY_ALPHABET_NUMBER", 0, 2, 1), - R.xml.kbd_qwerty_abc_123, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), - - // Godan keyboard. - GODAN_KANA( - new KeyboardSpecificationName("GODAN_KANA", 0, 1, 1), - R.xml.kbd_godan_kana, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.GODAN_TO_HIRAGANA, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - true, - CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), - - // HARDWARE QWERTY keyboard. - HARDWARE_QWERTY_KANA( - new KeyboardSpecificationName("HARDWARE_QWERTY_KANA", 0, 1, 0), - 0, - CompositionMode.HIRAGANA, - SpecialRomanjiTable.DEFAULT_TABLE, - SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, - false, - CrossingEdgeBehavior.DO_NOTHING), - - HARDWARE_QWERTY_ALPHABET( - new KeyboardSpecificationName("HARDWARE_QWERTY_ALPHABET", 0, 1, 0), - 0, - CompositionMode.HALF_ASCII, - SpecialRomanjiTable.DEFAULT_TABLE, - SpaceOnAlphanumeric.COMMIT, - false, - CrossingEdgeBehavior.DO_NOTHING), - - ; - - private final KeyboardSpecificationName specName; - private final int resourceId; - private final CompositionMode compositionMode; - private final SpecialRomanjiTable specialRomanjiTable; - private final SpaceOnAlphanumeric spaceOnAlphanumeric; - private final boolean kanaModifierInsensitiveConversion; - private final CrossingEdgeBehavior crossingEdgeBehavior; - - private KeyboardSpecification( - KeyboardSpecificationName specName, - int resourceId, - CompositionMode compositionMode, - SpecialRomanjiTable specialRomanjiTable, - SpaceOnAlphanumeric spaceOnAlphanumeric, - boolean kanaModifierInsensitiveConversion, - CrossingEdgeBehavior crossingEdgeBehavior) { - this.specName = Preconditions.checkNotNull(specName); - this.resourceId = resourceId; - this.compositionMode = Preconditions.checkNotNull(compositionMode); - this.specialRomanjiTable = Preconditions.checkNotNull(specialRomanjiTable); - this.spaceOnAlphanumeric = Preconditions.checkNotNull(spaceOnAlphanumeric); - this.kanaModifierInsensitiveConversion = kanaModifierInsensitiveConversion; - this.crossingEdgeBehavior = Preconditions.checkNotNull(crossingEdgeBehavior); - } - - public int getXmlLayoutResourceId() { - return resourceId; - } - - public CompositionMode getCompositionMode() { - return compositionMode; - } - - public KeyboardSpecificationName getKeyboardSpecificationName() { - return specName; - } - - public KeyboardSpecificationName getSpecName() { - return specName; - } - - public SpecialRomanjiTable getSpecialRomanjiTable() { - return specialRomanjiTable; - } - - public SpaceOnAlphanumeric getSpaceOnAlphanumeric() { - return spaceOnAlphanumeric; - } - - public boolean isKanaModifierInsensitiveConversion() { - return kanaModifierInsensitiveConversion; - } - - public CrossingEdgeBehavior getCrossingEdgeBehavior() { - return crossingEdgeBehavior; - } - } - - private final KeyboardSpecification specification; - private Optional sourceIdToKeyCode = Optional.absent(); - - public JapaneseKeyboard( - Optional contentDescription, - List rowList, float flickThreshold, KeyboardSpecification specification) { - super(Preconditions.checkNotNull(contentDescription), - Preconditions.checkNotNull(rowList), flickThreshold); - this.specification = Preconditions.checkNotNull(specification); - } - - public KeyboardSpecification getSpecification() { - return specification; - } - - /** - * Returns keyCode from {@code souceId}. - * - *

If not found, {@code Integer.MIN_VALUE} is returned. - */ - public int getKeyCode(int sourceId) { - ensureSourceIdToKeyCode(); - return sourceIdToKeyCode.get().get(sourceId, Integer.MIN_VALUE); - } - - private void ensureSourceIdToKeyCode() { - if (sourceIdToKeyCode.isPresent()) { - return; - } - SparseIntArray result = new SparseIntArray(); - for (Row row : getRowList()) { - for (Key key : row.getKeyList()) { - for (KeyState keyState : key.getKeyStates()) { - for (Direction direction : Direction.values()) { - Flick flick = keyState.getFlick(direction); - if (flick != null) { - KeyEntity keyEntity = flick.getKeyEntity(); - result.put(keyEntity.getSourceId(), keyEntity.getKeyCode()); - } - } - } - } - } - sourceIdToKeyCode = Optional.of(result); - } -} diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java b/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java deleted file mode 100644 index 19cde71a6..000000000 --- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2010-2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package org.mozc.android.inputmethod.japanese; - -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; -import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; -import org.mozc.android.inputmethod.japanese.keyboard.KeyboardParser; -import org.mozc.android.inputmethod.japanese.keyboard.Row; -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; - -import android.content.res.Resources; -import android.content.res.XmlResourceParser; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.List; - -/** - */ -public class JapaneseKeyboardParser extends KeyboardParser { - private final KeyboardSpecification specification; - - public JapaneseKeyboardParser( - Resources resources, XmlResourceParser parser, KeyboardSpecification specification, - int keyboardWidth, int keyboardHeight) { - super(resources, parser, keyboardWidth, keyboardHeight); - if (specification == null) { - throw new NullPointerException("specification is null."); - } - this.specification = specification; - } - - /** - * Parses a XML file and returns the keyboard instance. - * @return JapaneseKeyboard instance parsed from the resource. - * @throws XmlPullParserException is thrown if parsing is failed. - * @throws IOException is thrown if there is trouble to read data. - */ - @Override - public JapaneseKeyboard parseKeyboard() throws XmlPullParserException, IOException { - return JapaneseKeyboard.class.cast(super.parseKeyboard()); - } - - @Override - protected Keyboard buildKeyboard(Optional contentDescription, - List rowList, float flickThreshold) { - return new JapaneseKeyboard(Preconditions.checkNotNull(contentDescription), - Preconditions.checkNotNull(rowList), flickThreshold, - specification); - } -} diff --git a/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java b/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java index 8316583ba..a43c9c571 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java +++ b/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java @@ -29,6 +29,7 @@ package org.mozc.android.inputmethod.japanese; +import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; import org.mozc.android.inputmethod.japanese.keyboard.Flick; import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction; import org.mozc.android.inputmethod.japanese.keyboard.Key; @@ -37,8 +38,10 @@ import org.mozc.android.inputmethod.japanese.keyboard.KeyEventContext; import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler; import org.mozc.android.inputmethod.japanese.keyboard.KeyState; +import org.mozc.android.inputmethod.japanese.keyboard.PopUp; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchAction; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import android.view.MotionEvent; import android.view.View; @@ -54,6 +57,7 @@ * */ public class KeyEventButtonTouchListener implements OnTouchListener { + private final int sourceId; private final int keyCode; private KeyEventHandler keyEventHandler = null; @@ -95,9 +99,11 @@ protected void setKeyEventHandler(KeyEventHandler keyEventHandler) { * {@code keyCode}. * This is exported as package private for testing. */ - static Key createKey(View button, int sourceId, int keyCode) { - KeyEntity keyEntity = - new KeyEntity(sourceId, keyCode, KeyEntity.INVALID_KEY_CODE, 0, null, null, false, null); + @VisibleForTesting static Key createKey(View button, int sourceId, int keyCode) { + KeyEntity keyEntity = new KeyEntity( + sourceId, keyCode, KeyEntity.INVALID_KEY_CODE, true, 0, + Optional.absent(), false, + Optional.absent(), 0, 0, 0, 0); Flick flick = new Flick(Direction.CENTER, keyEntity); KeyState keyState = new KeyState("", @@ -105,9 +111,10 @@ static Key createKey(View button, int sourceId, int keyCode) { Collections.emptySet(), Collections.emptySet(), Collections.singletonList(flick)); - // Now, we support repetable keys only. + // Now, we support repeatable keys only. return new Key(0, 0, button.getWidth(), button.getHeight(), 0, 0, - true, false, false, Stick.EVEN, Collections.singletonList(keyState)); + true, false, Stick.EVEN, DrawableType.TWELVEKEYS_REGULAR_KEY_BACKGROUND, + Collections.singletonList(keyState)); } private static KeyEventContext createKeyEventContext( @@ -157,8 +164,9 @@ void onUp(View button, float x, float y, long timestamp) { if (keyEventHandler != null && keyEventContext != null) { keyEventContext.update(x, y, TouchAction.TOUCH_UP, timestamp); keyEventHandler.cancelDelayedKeyEvent(keyEventContext); + // TODO(hsumita): Confirm that we can put null as a touch event or not. keyEventHandler.sendKey(keyEventContext.getKeyCode(), - Collections.singletonList(keyEventContext.getTouchEvent())); + Collections.singletonList(keyEventContext.getTouchEvent().orNull())); keyEventHandler.sendRelease(keyEventContext.getPressedKeyCode()); } this.keyEventContext = null; diff --git a/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java b/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java index c42e6d3b7..f7e3e7be0 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java +++ b/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java @@ -32,6 +32,8 @@ import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.ModifierKey; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.SpecialKey; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; /** * Converts Androids's KeyEvent to Mozc's KeyEvent. @@ -52,7 +54,7 @@ public class KeycodeConverter { */ public interface KeyEventInterface { int getKeyCode(); - android.view.KeyEvent getNativeEvent(); + Optional getNativeEvent(); } private static final int ASCII_MIN = 32; // Space. @@ -112,6 +114,7 @@ public static ProtoCommands.KeyEvent getMozcKeyEvent(int keyCode) { } public static KeyEventInterface getKeyEventInterface(final android.view.KeyEvent keyEvent) { + Preconditions.checkNotNull(keyEvent); return new KeyEventInterface() { @Override @@ -120,8 +123,8 @@ public int getKeyCode() { } @Override - public android.view.KeyEvent getNativeEvent() { - return keyEvent; + public Optional getNativeEvent() { + return Optional.of(keyEvent); } }; } @@ -135,14 +138,14 @@ public int getKeyCode() { } @Override - public android.view.KeyEvent getNativeEvent() { - return null; + public Optional getNativeEvent() { + return Optional.absent(); } }; } public static boolean isMetaKey(android.view.KeyEvent keyEvent) { - int keyCode = keyEvent.getKeyCode(); + int keyCode = Preconditions.checkNotNull(keyEvent).getKeyCode(); return keyCode == android.view.KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == android.view.KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == android.view.KeyEvent.KEYCODE_CTRL_LEFT || diff --git a/src/android/tests/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParserTest.java b/src/android/src/com/google/android/inputmethod/japanese/LauncherActivity.java similarity index 55% rename from src/android/tests/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParserTest.java rename to src/android/src/com/google/android/inputmethod/japanese/LauncherActivity.java index 4b7a0ad3b..b8063cfbc 100644 --- a/src/android/tests/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParserTest.java +++ b/src/android/src/com/google/android/inputmethod/japanese/LauncherActivity.java @@ -29,37 +29,21 @@ package org.mozc.android.inputmethod.japanese; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; -import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.preference.MozcProxyActivity; +import org.mozc.android.inputmethod.japanese.preference.MozcProxyPreferenceActivity; -import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; -import android.test.InstrumentationTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; +import android.content.Intent; /** + * Activity to show launcher icon on the home screen. + * + *

MozcProxyPreferenceActivity is always active because it is invoked from system preference. + * This class might be deactivated to hide launcher icon. */ -public class JapaneseKeyboardParserTest extends InstrumentationTestCase { - @SmallTest - public void testParseKeyboard() - throws NotFoundException, XmlPullParserException, IOException { - // The only diff from KeyboardParser, the base of this class, is - // that the result type is JapaneseKeyboard, and it has a corresponding - // specification. - Resources resources = getInstrumentation().getTargetContext().getResources(); - JapaneseKeyboardParser parser = new JapaneseKeyboardParser( - resources, resources.getXml(R.xml.kbd_12keys_kana), - KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA, 480, 200); - - // ClassCastException shouldn't be raised. - JapaneseKeyboard keyboard = parser.parseKeyboard(); +public class LauncherActivity extends MozcProxyActivity { - // The keyboard should have specified Specification. - assertEquals(KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA, - keyboard.getSpecification()); + @Override + protected Intent getForwardIntent() { + return new Intent(this, MozcProxyPreferenceActivity.class); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardView.java b/src/android/src/com/google/android/inputmethod/japanese/LauncherIconVisibilityInitializer.java similarity index 60% rename from src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardView.java rename to src/android/src/com/google/android/inputmethod/japanese/LauncherIconVisibilityInitializer.java index 8b0e81de2..c7b6bca32 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/LauncherIconVisibilityInitializer.java @@ -29,43 +29,28 @@ package org.mozc.android.inputmethod.japanese; -import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView; +import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory; +import android.content.BroadcastReceiver; import android.content.Context; -import android.util.AttributeSet; +import android.content.Intent; /** - * Keyboard view for Japanese. - * - * The features dedicated to Japanese input are implemented in this class. - * + * A broadcast receiver to initialize launcher icon's visibility. */ -public class JapaneseKeyboardView extends KeyboardView { - public JapaneseKeyboardView(Context context) { - super(context); - } - - public JapaneseKeyboardView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public JapaneseKeyboardView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } +public class LauncherIconVisibilityInitializer extends BroadcastReceiver { - /** - * Sets a Keyboard. - * - * Internally this method delegates to super.setKeyboard(), which is protected scope. - * TODO(hidehiko): Rename following methods to {set,get}Keyboard, as covariant - * return value should be supported Java 1.5 or later. - * @param keyboard a keyboard instance to be set - */ - public void setJapaneseKeyboard(JapaneseKeyboard keyboard) { - super.setKeyboard(keyboard); + @Override + public void onReceive(Context context, Intent intent) { + if (shouldHandle(intent)) { + LauncherIconManagerFactory.getDefaultInstance().updateLauncherIconVisibility(context); + } } - public JapaneseKeyboard getJapaneseKeyboard() { - return JapaneseKeyboard.class.cast(super.getKeyboard()); + private boolean shouldHandle(Intent intent) { + String action = intent.getAction(); + return "android.intent.action.BOOT_COMPLETED".equals(action) + || "android.intent.action.MY_PACKAGE_REPLACED".equals(action) + || "android.intent.action.USER_INITIALIZE".equals(action); } -} +} \ No newline at end of file diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java b/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java index e5dd770d9..189f2feca 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java +++ b/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java @@ -31,7 +31,6 @@ import org.mozc.android.inputmethod.japanese.mushroom.MushroomUtil; import org.mozc.android.inputmethod.japanese.ui.MenuDialog.MenuDialogListener; -import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher; import com.google.common.base.Preconditions; import android.content.Context; @@ -45,12 +44,10 @@ */ class MozcMenuDialogListenerImpl implements MenuDialogListener { private final InputMethodService inputMethodService; - private final ImeSwitcher imeSwitcher; private boolean showInputMethodPicker = false; - MozcMenuDialogListenerImpl(InputMethodService inputMethodService, ImeSwitcher imeSwitcher) { + MozcMenuDialogListenerImpl(InputMethodService inputMethodService) { this.inputMethodService = Preconditions.checkNotNull(inputMethodService); - this.imeSwitcher = Preconditions.checkNotNull(imeSwitcher); } @Override @@ -85,13 +82,6 @@ public void onLaunchPreferenceActivitySelected(Context context) { context.startActivity(intent); } - @Override - public void onLaunchVoiceInputActivitySelected(Context context) { - if (!imeSwitcher.switchToVoiceIme("ja")) { - MozcLog.e("Voice IME for ja locale is not found."); - } - } - @Override public void onShowMushroomSelectionDialogSelected(Context context) { // Reset the composing text, otherwise the composing text will be committed automatically diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcService.java b/src/android/src/com/google/android/inputmethod/japanese/MozcService.java index 0cfa18e8c..17439b030 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/MozcService.java +++ b/src/android/src/com/google/android/inputmethod/japanese/MozcService.java @@ -31,22 +31,21 @@ import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackListener; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; import org.mozc.android.inputmethod.japanese.emoji.EmojiUtil; -import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard; import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboardSpecification; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.model.SelectionTracker; import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage.SymbolHistoryStorage; import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; import org.mozc.android.inputmethod.japanese.mushroom.MushroomResultProxy; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference; +import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Context.InputFieldType; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.DeletionRange; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.GenericStorageEntry.StorageType; @@ -56,9 +55,6 @@ import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment.Annotation; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand.UsageStatsEvent; import org.mozc.android.inputmethod.japanese.protobuf.ProtoConfig.Config; @@ -69,11 +65,13 @@ import org.mozc.android.inputmethod.japanese.session.SessionHandlerFactory; import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory; import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher; +import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; @@ -97,6 +95,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; @@ -209,6 +208,8 @@ public List getAllHistory(SymbolMajorCategory majorCategory) { @Override public void addHistory(SymbolMajorCategory majorCategory, String value) { + Preconditions.checkNotNull(majorCategory); + Preconditions.checkNotNull(value); sessionExecutor.insertToStorage( STORAGE_TYPE_MAP.get(majorCategory), value, @@ -217,17 +218,31 @@ public void addHistory(SymbolMajorCategory majorCategory, String value) { } // Called back from ViewManager - // Package private for testing. - class MozcEventListener implements ViewEventListener { + @VisibleForTesting class MozcEventListener implements ViewEventListener { @Override public void onConversionCandidateSelected(int candidateId, Optional rowIndex) { sessionExecutor.submitCandidate(candidateId, rowIndex, renderResultCallback); feedbackManager.fireFeedback(FeedbackEvent.CANDIDATE_SELECTED); } + @Override + public void onPageUp() { + sessionExecutor.pageUp(renderResultCallback); + feedbackManager.fireFeedback(FeedbackEvent.KEY_DOWN); + } + + @Override + public void onPageDown() { + sessionExecutor.pageDown(renderResultCallback); + feedbackManager.fireFeedback(FeedbackEvent.KEY_DOWN); + } + @Override public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String candidate, boolean updateHistory) { + Preconditions.checkNotNull(majorCategory); + Preconditions.checkNotNull(candidate); + // Directly commit the text. commitText(candidate); @@ -252,8 +267,8 @@ private void commitText(String text) { @Override public void onKeyEvent( - ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface keyEvent, - KeyboardSpecification keyboardSpecification, List touchEventList) { + @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface keyEvent, + @Nullable KeyboardSpecification keyboardSpecification, List touchEventList) { if (mozcKeyEvent == null && keyboardSpecification == null) { // We don't send a key event to Mozc native layer since {@code mozcKeyEvent} is null, and we // don't need to update the keyboard specification since {@code keyboardSpecification} is @@ -275,7 +290,7 @@ keyboardSpecification, getConfiguration(), } @Override - public void onUndo(List touchEventList) { + public void onUndo(List touchEventList) { sessionExecutor.undoOrRewind(touchEventList, renderResultCallback); } @@ -300,57 +315,73 @@ public void onExpandSuggestion() { } @Override - public void onShowMenuDialog(List touchEventList) { + public void onShowMenuDialog(List touchEventList) { sessionExecutor.touchEventUsageStatsEvent(touchEventList); } @Override - public void onShowSymbolInputView(List touchEventList) { - // Send request with (only) keyboard name for logging usage stats. - sessionExecutor.updateRequest( - MozcUtil.getRequestForKeyboard( - SymbolInputView.SPEC_NAME, - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - getConfiguration()), - touchEventList); + public void onShowSymbolInputView(List touchEventList) { + changeKeyboardSpecificationAndSendKey( + null, null, KeyboardSpecification.SYMBOL_NUMBER, getConfiguration(), + Collections.emptyList()); + viewManager.onShowSymbolInputView(); } @Override public void onCloseSymbolInputView() { - KeyboardSpecification specification = viewManager.getJapaneseKeyboardSpecification(); - sessionExecutor.updateRequest( - MozcUtil.getRequestForKeyboard( - specification.getKeyboardSpecificationName(), - Optional.of(specification.getSpecialRomanjiTable()), - Optional.of(specification.getSpaceOnAlphanumeric()), - Optional.of(specification.isKanaModifierInsensitiveConversion()), - Optional.of(specification.getCrossingEdgeBehavior()), - getConfiguration()), - Collections.emptyList()); + viewManager.onCloseSymbolInputView(); + // This callback is called in two ways: one is from touch event on symbol input view. + // The other is from onKeyDown event by hardware keyboard. ViewManager.isNarrowMode() + // is abused to distinguish these two triggers where its true value indicates that + // onCloseSymbolInputView() is called on hardware keyboard event. In the case of hardware + // keyboard event, keyboard specification has been already updated so we shouldn't update it. + if (!viewManager.isNarrowMode()) { + changeKeyboardSpecificationAndSendKey( + null, null, viewManager.getKeyboardSpecification(), getConfiguration(), + Collections.emptyList()); + } } @Override public void onHardwareKeyboardCompositionModeChange(CompositionSwitchMode mode) { - CompositionMode oldMode = hardwareKeyboard.getCompositionMode(); - hardwareKeyboard.setCompositionMode(mode); - CompositionMode newMode = hardwareKeyboard.getCompositionMode(); - if (oldMode != newMode) { - viewManager.setHardwareKeyboardCompositionMode(newMode); - sendKeyWithKeyboardSpecification( - null, null, - hardwareKeyboard.getKeyboardSpecification(), - getConfiguration(), - Collections.emptyList()); - } + viewManager.switchHardwareKeyboardCompositionMode(mode); } @Override public void onActionKey() { // false means that the key is for Action and not ENTER. - sendDefaultEditorAction(false); + sendEditorAction(false); + } + + @Override + public void onNarrowModeChanged(boolean newNarrowMode) { + if (!newNarrowMode) { + // Hardware keyboard to software keyboard transition: Submit composition. + sessionExecutor.submit(renderResultCallback); + } + updateImposedConfig(); + } + + @Override + public void onUpdateKeyboardLayoutAdjustment( + ViewManagerInterface.LayoutAdjustment layoutAdjustment) { + Preconditions.checkNotNull(layoutAdjustment); + Configuration configuration = getConfiguration(); + if (sharedPreferences == null || configuration == null) { + return; + } + boolean isLandscapeKeyboardSettingActive = + PreferenceUtil.isLandscapeKeyboardSettingActive( + sharedPreferences, configuration.orientation); + String key; + if (isLandscapeKeyboardSettingActive) { + key = PreferenceUtil.PREF_LANDSCAPE_LAYOUT_ADJUSTMENT_KEY; + } else { + key = PreferenceUtil.PREF_PORTRAIT_LAYOUT_ADJUSTMENT_KEY; + } + sharedPreferences.edit() + .putString(key, layoutAdjustment.toString()) + .apply(); } } @@ -360,16 +391,18 @@ public void onActionKey() { private class RenderResultCallback implements SessionExecutor.EvaluationCallback { @Override - public void onCompleted(Command command, @Nullable KeyEventInterface triggeringKeyEvent) { - Preconditions.checkNotNull(command); - if (command.getInput().getCommand().getType() != - SessionCommand.CommandType.EXPAND_SUGGESTION) { + public void onCompleted( + Optional command, Optional triggeringKeyEvent) { + Preconditions.checkArgument(Preconditions.checkNotNull(command).isPresent()); + Preconditions.checkNotNull(triggeringKeyEvent); + if (command.get().getInput().getCommand().getType() + != SessionCommand.CommandType.EXPAND_SUGGESTION) { // For expanding suggestions, we don't need to update our rendering result. - renderInputConnection(command, triggeringKeyEvent); + renderInputConnection(command.get(), triggeringKeyEvent.orNull()); } // Transit to narrow mode if required (e.g., Typed 'a' key from h/w keyboard). - viewManager.maybeTransitToNarrowMode(command, triggeringKeyEvent); - viewManager.render(command); + viewManager.maybeTransitToNarrowMode(command.get(), triggeringKeyEvent.orNull()); + viewManager.render(command.get()); } } @@ -380,10 +413,10 @@ public void onCompleted(Command command, @Nullable KeyEventInterface triggeringK class SendKeyToApplicationCallback implements SessionExecutor.EvaluationCallback { @Override - public void onCompleted(@Nullable Command command, - @Nullable KeyEventInterface triggeringKeyEvent) { - Preconditions.checkArgument(command == null); - sendKeyEvent(triggeringKeyEvent); + public void onCompleted(Optional command, + Optional triggeringKeyEvent) { + Preconditions.checkArgument(!Preconditions.checkNotNull(command).isPresent()); + sendKeyEvent(triggeringKeyEvent.orNull()); } } @@ -393,10 +426,11 @@ public void onCompleted(@Nullable Command command, private class SendKeyToViewCallback implements SessionExecutor.EvaluationCallback { @Override - public void onCompleted(@Nullable Command command, KeyEventInterface triggeringKeyEvent) { - Preconditions.checkArgument(command == null); - Preconditions.checkNotNull(triggeringKeyEvent); - viewManager.consumeKeyOnViewSynchronously(triggeringKeyEvent.getNativeEvent()); + public void onCompleted( + Optional command, Optional triggeringKeyEvent) { + Preconditions.checkArgument(!Preconditions.checkNotNull(command).isPresent()); + Preconditions.checkArgument(Preconditions.checkNotNull(triggeringKeyEvent).isPresent()); + viewManager.consumeKeyOnViewSynchronously(triggeringKeyEvent.get().getNativeEvent().orNull()); } } @@ -417,6 +451,7 @@ public boolean handleMessage(Message msg) { /** * We need to send SYNC_DATA command periodically. This class handles it. */ + @SuppressLint("HandlerLeak") private class SendSyncDataCommandHandler extends Handler { /** * The current period of sending SYNC_DATA is 15 mins (as same as desktop version). @@ -439,6 +474,7 @@ public void handleMessage(Message msg) { * This class handles callback operation. * Posting and removing messages should be done in appropriate point. */ + @SuppressLint("HandlerLeak") private class MemoryTrimmingHandler extends Handler { /** @@ -468,16 +504,21 @@ public void handleMessage(Message msg) { // Focused segment's attribute. @VisibleForTesting static final CharacterStyle SPAN_CONVERT_HIGHLIGHT = - new BackgroundColorSpan(0x8888FFFF); + new BackgroundColorSpan(0x66EF3566); + + // Background color span for non-focused conversion segment. + // We don't create a static CharacterStyle instance since there are multiple segments at the same + // time. Otherwise, segments except for the last one cannot have style. + @VisibleForTesting static final int CONVERT_NORMAL_COLOR = 0x19EF3566; // Cursor position. // Note that InputConnection seems not to be able to show cursor. This is a workaround. @VisibleForTesting static final CharacterStyle SPAN_BEFORE_CURSOR = - new BackgroundColorSpan(0x88FF88FF); + new BackgroundColorSpan(0x664DB6AC); - // To hide a caret, we use non-transparent background for partial conversion. - private static final CharacterStyle SPAN_PARTIAL_SUGGESTION_COLOR = - new BackgroundColorSpan(0xFFFFE0E0); + // Background color span for partial conversion. + @VisibleForTesting static final CharacterStyle SPAN_PARTIAL_SUGGESTION_COLOR = + new BackgroundColorSpan(0x194DB6AC); // Underline. @VisibleForTesting static final CharacterStyle SPAN_UNDERLINE = new UnderlineSpan(); @@ -535,10 +576,6 @@ public void handleMessage(Message msg) { @VisibleForTesting KeyboardSpecification currentKeyboardSpecification = KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA; - // Non-final for testing - // TODO(matsuzakit): Setting this in onCreateInternal might be more consistent. - @VisibleForTesting HardwareKeyboard hardwareKeyboard = new HardwareKeyboard(); - // Current HardKeyboardHidden configuration value. // This is updated only when onConfigurationChanged is called and // Configuration.HARDKEYBOARDHIDDEN_* differs to this. @@ -553,6 +590,15 @@ public void handleMessage(Message msg) { // Held for testing. private ViewEventListener eventListener; + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + public MozcService() { + super(); + if (Build.VERSION.SDK_INT >= 17) { + enableHardwareAcceleration(); + } + } + @Override public void onBindInput() { super.onBindInput(); @@ -565,6 +611,13 @@ public void onUnbindInput() { super.onUnbindInput(); } + @Override + public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + if (viewManager != null) { + viewManager.setCursorAnchorInfo(cursorAnchorInfo); + } + } + @Override public MozcInputMethod onCreateInputMethodInterface() { return new MozcInputMethod(); @@ -579,9 +632,10 @@ public void onCreate() { // Callback object mainly used by views. MozcEventListener eventListener = new MozcEventListener(); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + Preconditions.checkNotNull(sharedPreferences); SessionExecutor sessionExecutor = SessionExecutor.getInstanceInitializedIfNecessary( - new SessionHandlerFactory(sharedPreferences), this); + new SessionHandlerFactory(Optional.of(sharedPreferences)), this); onCreateInternal(eventListener, null, sharedPreferences, getConfiguration(), sessionExecutor); @@ -598,9 +652,9 @@ public void onDestroy() { } @VisibleForTesting - void onCreateInternal(ViewEventListener eventListener, ViewManagerInterface viewManager, - SharedPreferences sharedPreferences, Configuration deviceConfiguration, - SessionExecutor sessionExecutor) { + void onCreateInternal(ViewEventListener eventListener, @Nullable ViewManagerInterface viewManager, + @Nullable SharedPreferences sharedPreferences, + Configuration deviceConfiguration, SessionExecutor sessionExecutor) { super.onCreate(); Context context = getApplicationContext(); @@ -614,8 +668,8 @@ void onCreateInternal(ViewEventListener eventListener, ViewManagerInterface view prepareOnce(eventListener, symbolHistoryStorage, viewManager, sharedPreferences); prepareEveryTime(sharedPreferences, deviceConfiguration); - if (propagatedClientSidePreference == null || - propagatedClientSidePreference.getHardwareKeyMap() == null) { + if (propagatedClientSidePreference == null + || propagatedClientSidePreference.getHardwareKeyMap() == null) { HardwareKeyboardSpecification.maybeSetDetectedHardwareKeyMap( sharedPreferences, deviceConfiguration, false); } @@ -630,14 +684,15 @@ void onCreateInternal(ViewEventListener eventListener, ViewManagerInterface view * Prepares something which should be done every time when the session is newly created. */ private void prepareEveryTime( - SharedPreferences sharedPreferences, Configuration deviceConfiguration) { - boolean isLogging = sharedPreferences != null && - sharedPreferences.getBoolean(PREF_TWEAK_LOGGING_PROTOCOL_BUFFERS, false); + @Nullable SharedPreferences sharedPreferences, Configuration deviceConfiguration) { + boolean isLogging = sharedPreferences != null + && sharedPreferences.getBoolean(PREF_TWEAK_LOGGING_PROTOCOL_BUFFERS, false); // Force to initialize here. - sessionExecutor.reset(new SessionHandlerFactory(sharedPreferences), this); + sessionExecutor.reset( + new SessionHandlerFactory(Optional.fromNullable(sharedPreferences)), this); sessionExecutor.setLogging(isLogging); - updateImposedConfig(getConfiguration()); + updateImposedConfig(); viewManager.onConfigurationChanged(getConfiguration()); // Make sure that the server and the client have the same keyboard specification. // User preference's keyboard will be set after this step. @@ -646,11 +701,12 @@ private void prepareEveryTime( Collections.emptyList()); if (sharedPreferences != null) { propagateClientSidePreference( - new ClientSidePreference(sharedPreferences, deviceConfiguration.orientation)); + new ClientSidePreference( + sharedPreferences, getResources(), deviceConfiguration.orientation)); // TODO(hidehiko): here we just set the config based on preferences. When we start // to support sync on Android, we need to revisit the config related design. sessionExecutor.setConfig(ConfigUtil.toConfig(sharedPreferences)); - sessionExecutor.preferenceUsageStatsEvent(sharedPreferences); + sessionExecutor.preferenceUsageStatsEvent(sharedPreferences, getResources()); } maybeSetNarrowMode(deviceConfiguration); @@ -661,16 +717,17 @@ private void prepareEveryTime( */ private void prepareOnce(ViewEventListener eventListener, SymbolHistoryStorage symbolHistoryStorage, - ViewManagerInterface viewManager, - SharedPreferences sharedPreferences) { + @Nullable ViewManagerInterface viewManager, + @Nullable SharedPreferences sharedPreferences) { Context context = getApplicationContext(); - boolean omitWelcomeActivity = false; Optional forwardIntent = ApplicationInitializerFactory.createInstance(this).initialize( - omitWelcomeActivity, + MozcUtil.isSystemApplication(context), MozcUtil.isDevChannel(context), DependencyFactory.getDependency(getApplicationContext()).isWelcomeActivityPreferrable(), - MozcUtil.getAbiIndependentVersionCode(context)); + MozcUtil.getAbiIndependentVersionCode(context), + LauncherIconManagerFactory.getDefaultInstance(), + PreferenceUtil.getDefaultPreferenceManagerStatic()); if (forwardIntent.isPresent()) { startActivity(forwardIntent.get()); } @@ -684,7 +741,7 @@ private void prepareOnce(ViewEventListener eventListener, eventListener, symbolHistoryStorage, imeSwitcher, - new MozcMenuDialogListenerImpl(this, imeSwitcher)); + new MozcMenuDialogListenerImpl(this)); } // Setup FeedbackManager. @@ -714,7 +771,7 @@ public View onCreateInputView() { return inputView; } - void resetContext() { + private void resetContext() { if (sessionExecutor != null) { sessionExecutor.resetContext(); } @@ -740,16 +797,16 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { // Update full screen mode, because the application may be changed. viewManager.setFullscreenMode( - applicationCompatibility.isFullScreenModeSupported() && - propagatedClientSidePreference != null && - propagatedClientSidePreference.isFullscreenMode()); + applicationCompatibility.isFullScreenModeSupported() + && propagatedClientSidePreference != null + && propagatedClientSidePreference.isFullscreenMode()); // Some applications, e.g. gmail or maps, send onStartInput with restarting = true, when a user // rotates a device. In such cases, we don't want to update caret positions, nor reset // the context basically. However, some other applications, such as one with a webview widget // like a browser, send onStartInput with restarting = true, too. Unfortunately, // there seems no way to figure out which one causes this invocation. - // So, as a point of compromise, we reset the context everytime here. Also, we'll send + // So, as a point of compromise, we reset the context every time here. Also, we'll send // finishComposingText as well, in case the new attached field has already had composing text // (we hit such a situation on webview, too). // See also onConfigurationChanged for caret position handling on gmail-like applications' @@ -778,7 +835,7 @@ public void onStartInput(EditorInfo attribute, boolean restarting) { * field, commit it. Then, (regardless of whether there exists pending result,) clears * all remaining pending result. */ - static void maybeCommitMushroomResult(EditorInfo attribute, InputConnection connection) { + private static void maybeCommitMushroomResult(EditorInfo attribute, InputConnection connection) { if (connection == null) { return; } @@ -796,10 +853,20 @@ static void maybeCommitMushroomResult(EditorInfo attribute, InputConnection conn } } + @SuppressLint("NewApi") + private static boolean enableCursorAnchorInfo(InputConnection connection) { + Preconditions.checkNotNull(connection); + if (Build.VERSION.SDK_INT < 21) { + return false; + } + return connection.requestCursorUpdates( + InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR); + } + /** * @return true if connected view is WebEditText (or the application pretends it) */ - boolean isWebEditText(EditorInfo editorInfo) { + private boolean isWebEditText(EditorInfo editorInfo) { if (editorInfo == null) { return false; } @@ -816,16 +883,26 @@ boolean isWebEditText(EditorInfo editorInfo) { @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { + InputConnection inputConnection = getCurrentInputConnection(); + if (inputConnection != null && Build.VERSION.SDK_INT >= 21) { + viewManager.setCursorAnchorInfoEnabled(enableCursorAnchorInfo(inputConnection)); + updateImposedConfig(); + } + viewManager.setTextForActionButton(getTextForImeAction(attribute.imeOptions)); viewManager.setEditorInfo(attribute); + // updateXxxxxButtonEnabled cannot be placed in onStartInput because + // the view might be created after onStartInput with *reset* status. + viewManager.updateGlobeButtonEnabled(); + viewManager.updateMicrophoneButtonEnabled(); } static InputFieldType getInputFieldType(EditorInfo attribute) { int inputType = attribute.inputType; - int inputClass = inputType & InputType.TYPE_MASK_CLASS; - if (MozcUtil.isPasswordField(attribute)) { + if (MozcUtil.isPasswordField(inputType)) { return InputFieldType.PASSWORD; } + int inputClass = inputType & InputType.TYPE_MASK_CLASS; if (inputClass == InputType.TYPE_CLASS_PHONE) { return InputFieldType.TEL; } @@ -867,6 +944,8 @@ public boolean onGenericMotionEvent(MotionEvent event) { return super.onGenericMotionEvent(event); } + @SuppressLint("DefaultLocale") + @VisibleForTesting boolean onKeyDownInternal(int keyCode, KeyEvent event, Configuration configuration) { if (MozcLog.isLoggable(Log.DEBUG)) { MozcLog.d( @@ -893,7 +972,7 @@ boolean onKeyDownInternal(int keyCode, KeyEvent event, Configuration configurati return super.onKeyDown(keyCode, event); } - // Push the event to the asyncronous execution queue if it should be processed + // Push the event to the asynchronous execution queue if it should be processed // directly in the view. if (viewManager.isKeyConsumedOnViewAsynchronously(event)) { sessionExecutor.sendKeyEvent(KeycodeConverter.getKeyEventInterface(event), @@ -902,28 +981,17 @@ boolean onKeyDownInternal(int keyCode, KeyEvent event, Configuration configurati } // Lazy evaluation. - // If hardware keybaord is not set in the preference screen, + // If hardware keyboard is not set in the preference screen, // set it based on the configuration. - if (propagatedClientSidePreference == null || - propagatedClientSidePreference.getHardwareKeyMap() == null) { + if (propagatedClientSidePreference == null + || propagatedClientSidePreference.getHardwareKeyMap() == null) { HardwareKeyboardSpecification.maybeSetDetectedHardwareKeyMap( sharedPreferences, configuration, true); } // Here we decided to send the event to the server. - // Maybe update the composition mode based on the event. - // For example, zen/han key toggles the composition mode (hiragana <--> alphabet). - CompositionMode compositionMode = hardwareKeyboard.getCompositionMode(); - hardwareKeyboard.setCompositionModeByKey(event); - CompositionMode currentCompositionMode = hardwareKeyboard.getCompositionMode(); - if (currentCompositionMode != compositionMode) { - viewManager.setHardwareKeyboardCompositionMode(currentCompositionMode); - } - sendKeyWithKeyboardSpecification( - hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event), - hardwareKeyboard.getKeyboardSpecification(), configuration, - Collections.emptyList()); + viewManager.onHardwareKeyEvent(event); return true; } @@ -973,10 +1041,15 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { */ @VisibleForTesting void sendKeyWithKeyboardSpecification( - ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface event, - KeyboardSpecification keyboardSpecification, Configuration configuration, - List touchEventList) { - if (currentKeyboardSpecification != keyboardSpecification) { + @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface event, + @Nullable KeyboardSpecification keyboardSpecification, Configuration configuration, + List touchEventList) { + if (keyboardSpecification != null && currentKeyboardSpecification != keyboardSpecification) { + // Submit composition on the transition from software KB to hardware KB by key event. + if (!currentKeyboardSpecification.isHardwareKeyboard() + && keyboardSpecification.isHardwareKeyboard()) { + sessionExecutor.submit(renderResultCallback); + } changeKeyboardSpecificationAndSendKey( mozcKeyEvent, event, keyboardSpecification, configuration, touchEventList); updateStatusIcon(); @@ -991,31 +1064,26 @@ void sendKeyWithKeyboardSpecification( } /** - * Sends Request for changing keybaord setting to mozc server and sends key. + * Sends Request for changing keyboard setting to mozc server and sends key. */ private void changeKeyboardSpecificationAndSendKey( - ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface event, + @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface event, KeyboardSpecification keyboardSpecification, Configuration configuration, - List touchEventList) { + List touchEventList) { // Send Request to change composition table. sessionExecutor.updateRequest( - MozcUtil.getRequestForKeyboard( - keyboardSpecification.getKeyboardSpecificationName(), - Optional.of(keyboardSpecification.getSpecialRomanjiTable()), - Optional.of(keyboardSpecification.getSpaceOnAlphanumeric()), - Optional.of(keyboardSpecification.isKanaModifierInsensitiveConversion()), - Optional.of(keyboardSpecification.getCrossingEdgeBehavior()), - configuration), + MozcUtil.getRequestBuilder(getResources(), keyboardSpecification, configuration).build(), touchEventList); if (mozcKeyEvent == null) { // Change composition mode. - sessionExecutor.switchInputMode(event, keyboardSpecification.getCompositionMode(), - renderResultCallback); + sessionExecutor.switchInputMode( + Optional.fromNullable(event), keyboardSpecification.getCompositionMode(), + renderResultCallback); } else { // Send key with composition mode change. sessionExecutor.sendKey( ProtoCommands.KeyEvent.newBuilder(mozcKeyEvent) - .setMode(keyboardSpecification.getCompositionMode()).build(), + .setMode(keyboardSpecification.getCompositionMode()).build(), event, touchEventList, renderResultCallback); } currentKeyboardSpecification = keyboardSpecification; @@ -1035,7 +1103,7 @@ private void updateStatusIcon() { /** * Shows the status icon basing on the current keyboard spec. */ - void showStatusIcon() { + private void showStatusIcon() { switch (currentKeyboardSpecification.getCompositionMode()) { case HIRAGANA: showStatusIcon(R.drawable.status_icon_hiragana); @@ -1051,6 +1119,17 @@ public boolean onEvaluateFullscreenMode() { return viewManager.isFullscreenMode(); } + @Override + public boolean onShowInputRequested(int flags, boolean configChange) { + boolean result = super.onShowInputRequested(flags, configChange); + boolean isHardwareKeyboardConnected = + getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS; + // Original result becomes false when a hardware keyboard is connected. + // This means that the window won't be shown in such situation. + // We want to show it even with a hardware keyboard so override the result here. + return result || isHardwareKeyboardConnected; + } + @Override public void onWindowShown() { showStatusIcon(); @@ -1070,7 +1149,7 @@ null, null, currentKeyboardSpecification, getConfiguration(), public void onWindowHidden() { // "Hiding IME's window" is very similar to "Turning off IME" for PC. // Thus - // - Commiting composing text. + // - Committing composing text. // - Removing all pending messages. // - Resetting Mozc server // are needed. @@ -1096,6 +1175,7 @@ public void onWindowHidden() { * @param keyEvent Trigger event for this calling. When direct input is * needed, this event is sent to InputConnection. */ + @VisibleForTesting void renderInputConnection(Command command, @Nullable KeyEventInterface keyEvent) { Preconditions.checkNotNull(command); @@ -1115,8 +1195,8 @@ void renderInputConnection(Command command, @Nullable KeyEventInterface keyEvent // case, the command is consumed by Mozc server and the application cannot get the key event. // To avoid such situation, we should send the key event back to application. b/13238551 // The command itself is consumed by Mozc server, so we should NOT put a return statement here. - if (keyEvent != null && keyEvent.getNativeEvent() != null && - KeycodeConverter.isMetaKey(keyEvent.getNativeEvent())) { + if (keyEvent != null && keyEvent.getNativeEvent().isPresent() + && KeycodeConverter.isMetaKey(keyEvent.getNativeEvent().get())) { sendKeyEvent(keyEvent); } @@ -1136,7 +1216,8 @@ void renderInputConnection(Command command, @Nullable KeyEventInterface keyEvent } } - static KeyEvent createKeyEvent(KeyEvent original, long eventTime, int action, int repeatCount) { + private static KeyEvent createKeyEvent( + KeyEvent original, long eventTime, int action, int repeatCount) { return new KeyEvent( original.getDownTime(), eventTime, action, original.getKeyCode(), repeatCount, original.getMetaState(), original.getDeviceId(), original.getScanCode(), @@ -1146,7 +1227,7 @@ static KeyEvent createKeyEvent(KeyEvent original, long eventTime, int action, in /** * Sends the {@code KeyEvent}, which is not consumed by the mozc server. */ - void sendKeyEvent(KeyEventInterface keyEvent) { + @VisibleForTesting void sendKeyEvent(KeyEventInterface keyEvent) { if (keyEvent == null) { return; } @@ -1159,24 +1240,24 @@ void sendKeyEvent(KeyEventInterface keyEvent) { } // Following code is to fallback to target activity. - KeyEvent nativeKeyEvent = keyEvent.getNativeEvent(); + Optional nativeKeyEvent = keyEvent.getNativeEvent(); InputConnection inputConnection = getCurrentInputConnection(); - if (nativeKeyEvent != null && inputConnection != null) { + if (nativeKeyEvent.isPresent() && inputConnection != null) { // Meta keys are from this.onKeyDown/Up so fallback each time. - if (KeycodeConverter.isMetaKey(nativeKeyEvent)) { + if (KeycodeConverter.isMetaKey(nativeKeyEvent.get())) { inputConnection.sendKeyEvent(createKeyEvent( - nativeKeyEvent, MozcUtil.getUptimeMillis(), - nativeKeyEvent.getAction(), nativeKeyEvent.getRepeatCount())); + nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), + nativeKeyEvent.get().getAction(), nativeKeyEvent.get().getRepeatCount())); return; } // Other keys are from this.onKeyDown so create dummy Down/Up events. inputConnection.sendKeyEvent(createKeyEvent( - nativeKeyEvent, MozcUtil.getUptimeMillis(), KeyEvent.ACTION_DOWN, 0)); + nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), KeyEvent.ACTION_DOWN, 0)); inputConnection.sendKeyEvent(createKeyEvent( - nativeKeyEvent, MozcUtil.getUptimeMillis(), KeyEvent.ACTION_UP, 0)); + nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), KeyEvent.ACTION_UP, 0)); return; } @@ -1206,13 +1287,33 @@ private boolean maybeProcessActionKey(int keyCode) { if (keyCode != KeyEvent.KEYCODE_ENTER || !isInputViewShown()) { return false; } + return sendEditorAction(true); + } - // Fall back to EditorAction. Note that the keyCode is ENTER here, so set the fromEnterKey - // argument true. - return sendDefaultEditorAction(true); + /** + * Sends editor action to {@code InputConnection}. + *

+ * The difference from {@link InputMethodService#sendDefaultEditorAction(boolean)} is + * that if custom action label is specified {@code EditorInfo#actionId} is sent instead. + */ + private boolean sendEditorAction(boolean fromEnterKey) { + // If custom action label is specified (=non-null), special action id is also specified. + // If there is no IME_FLAG_NO_ENTER_ACTION option, we should send the id to the InputConnection. + EditorInfo editorInfo = getCurrentInputEditorInfo(); + if (editorInfo != null + && (editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0 + && editorInfo.actionLabel != null) { + InputConnection inputConnection = getCurrentInputConnection(); + if (inputConnection != null) { + inputConnection.performEditorAction(editorInfo.actionId); + return true; + } + } + // No custom action label is specified. Fall back to default EditorAction. + return sendDefaultEditorAction(fromEnterKey); } - static void maybeDeleteSurroundingText(Output output, InputConnection inputConnection) { + private static void maybeDeleteSurroundingText(Output output, InputConnection inputConnection) { if (!output.hasDeletionRange()) { return; } @@ -1232,7 +1333,7 @@ static void maybeDeleteSurroundingText(Output output, InputConnection inputConne } } - static void maybeCommitText(Output output, InputConnection inputConnection) { + private static void maybeCommitText(Output output, InputConnection inputConnection) { if (!output.hasResult()) { return; } @@ -1245,8 +1346,8 @@ static void maybeCommitText(Output output, InputConnection inputConnection) { int position = MozcUtil.CURSOR_POSITION_TAIL; if (output.getResult().hasCursorOffset()) { - if (output.getResult().getCursorOffset() == - -outputText.codePointCount(0, outputText.length())) { + if (output.getResult().getCursorOffset() + == -outputText.codePointCount(0, outputText.length())) { position = MozcUtil.CURSOR_POSITION_HEAD; } else { MozcLog.e("Unsupported position: " + output.getResult().toString()); @@ -1258,7 +1359,7 @@ static void maybeCommitText(Output output, InputConnection inputConnection) { } } - void setComposingText(Command command, InputConnection inputConnection) { + private void setComposingText(Command command, InputConnection inputConnection) { Preconditions.checkNotNull(command); Preconditions.checkNotNull(inputConnection); @@ -1274,8 +1375,8 @@ void setComposingText(Command command, InputConnection inputConnection) { // To avoid from this issue, we don't clear the composing text if the input // is SWITCH_INPUT_MODE. Input input = command.getInput(); - if (input.getType() != Input.CommandType.SEND_COMMAND || - input.getCommand().getType() != SessionCommand.CommandType.SWITCH_INPUT_MODE) { + if (input.getType() != Input.CommandType.SEND_COMMAND + || input.getCommand().getType() != SessionCommand.CommandType.SWITCH_INPUT_MODE) { if (!inputConnection.setComposingText("", 0)) { MozcLog.e("Failed to set composing text."); } @@ -1289,14 +1390,6 @@ void setComposingText(Command command, InputConnection inputConnection) { SpannableStringBuilder builder = new SpannableStringBuilder(); for (Segment segment : preedit.getSegmentList()) { builder.append(segment.getValue()); - if (segment.hasAnnotation() && segment.getAnnotation() == Annotation.HIGHLIGHT) { - // Highlight for the focused conversion part. - builder.setSpan( - SPAN_CONVERT_HIGHLIGHT, - builder.length() - segment.getValue().length(), - builder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } } // Set underline for all the preedit text. @@ -1304,18 +1397,30 @@ void setComposingText(Command command, InputConnection inputConnection) { // Draw cursor if in composition mode. int cursor = preedit.getCursor(); - if (!(output.hasAllCandidateWords() && - output.getAllCandidateWords().hasCategory() && - output.getAllCandidateWords().getCategory() == ProtoCandidates.Category.CONVERSION)) { + int spanFlags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING; + if (output.hasAllCandidateWords() + && output.getAllCandidateWords().hasCategory() + && output.getAllCandidateWords().getCategory() == ProtoCandidates.Category.CONVERSION) { + int offsetInString = 0; + for (Segment segment : preedit.getSegmentList()) { + int length = segment.getValue().length(); + builder.setSpan( + segment.hasAnnotation() && segment.getAnnotation() == Annotation.HIGHLIGHT + ? SPAN_CONVERT_HIGHLIGHT + : CharacterStyle.class.cast(new BackgroundColorSpan(CONVERT_NORMAL_COLOR)), + offsetInString, offsetInString + length, spanFlags); + offsetInString += length; + } + } else { // We cannot show system cursor inside preedit here. // Instead we change text style before the preedit's cursor. + int cursorOffsetInString = builder.toString().offsetByCodePoints(0, cursor); if (cursor != builder.length()) { - // This condition is workaround not to show unexpected background color for EditText. - builder.setSpan(SPAN_PARTIAL_SUGGESTION_COLOR, cursor, builder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(SPAN_PARTIAL_SUGGESTION_COLOR, cursorOffsetInString, builder.length(), + spanFlags); } if (cursor > 0) { - builder.setSpan(SPAN_BEFORE_CURSOR, 0, cursor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(SPAN_BEFORE_CURSOR, 0, cursorOffsetInString, spanFlags); } } @@ -1327,7 +1432,7 @@ void setComposingText(Command command, InputConnection inputConnection) { } } - void maybeSetSelection(Output output, InputConnection inputConnection) { + private void maybeSetSelection(Output output, InputConnection inputConnection) { if (!output.hasPreedit()) { return; } @@ -1382,68 +1487,71 @@ private static int getPreeditLength(Preedit preedit) { return; } ClientSidePreference oldPreference = propagatedClientSidePreference; - if (oldPreference == null || - oldPreference.isHapticFeedbackEnabled() != newPreference.isHapticFeedbackEnabled()) { + if (oldPreference == null + || oldPreference.isHapticFeedbackEnabled() != newPreference.isHapticFeedbackEnabled()) { feedbackManager.setHapticFeedbackEnabled(newPreference.isHapticFeedbackEnabled()); } - if (oldPreference == null || - oldPreference.getHapticFeedbackDuration() != newPreference.getHapticFeedbackDuration()) { + if (oldPreference == null + || oldPreference.getHapticFeedbackDuration() != newPreference.getHapticFeedbackDuration()) { feedbackManager.setHapticFeedbackDuration(newPreference.getHapticFeedbackDuration()); } - if (oldPreference == null || - oldPreference.isSoundFeedbackEnabled() != newPreference.isSoundFeedbackEnabled()) { + if (oldPreference == null + || oldPreference.isSoundFeedbackEnabled() != newPreference.isSoundFeedbackEnabled()) { feedbackManager.setSoundFeedbackEnabled(newPreference.isSoundFeedbackEnabled()); } - if (oldPreference == null || - oldPreference.getSoundFeedbackVolume() != newPreference.getSoundFeedbackVolume()) { - // The default value is 0.1f. In order to set the 50 to the default value, divide the - // preference value by 500f heuristically. - feedbackManager.setSoundFeedbackVolume(newPreference.getSoundFeedbackVolume() / 500f); + if (oldPreference == null + || oldPreference.getSoundFeedbackVolume() != newPreference.getSoundFeedbackVolume()) { + // The default value is 0.4f. In order to set the 50 to the default value, divide the + // preference value by 125f heuristically. + feedbackManager.setSoundFeedbackVolume(newPreference.getSoundFeedbackVolume() / 125f); } - if (oldPreference == null || - oldPreference.isPopupFeedbackEnabled() != newPreference.isPopupFeedbackEnabled()) { + if (oldPreference == null + || oldPreference.isPopupFeedbackEnabled() != newPreference.isPopupFeedbackEnabled()) { viewManager.setPopupEnabled(newPreference.isPopupFeedbackEnabled()); } - if (oldPreference == null || - oldPreference.getKeyboardLayout() != newPreference.getKeyboardLayout()) { + if (oldPreference == null + || oldPreference.getKeyboardLayout() != newPreference.getKeyboardLayout()) { viewManager.setKeyboardLayout(newPreference.getKeyboardLayout()); } - if (oldPreference == null || - oldPreference.getInputStyle() != newPreference.getInputStyle()) { + if (oldPreference == null + || oldPreference.getInputStyle() != newPreference.getInputStyle()) { viewManager.setInputStyle(newPreference.getInputStyle()); } - if (oldPreference == null || - oldPreference.isQwertyLayoutForAlphabet() != newPreference.isQwertyLayoutForAlphabet()) { + if (oldPreference == null + || oldPreference.isQwertyLayoutForAlphabet() != newPreference.isQwertyLayoutForAlphabet()) { viewManager.setQwertyLayoutForAlphabet(newPreference.isQwertyLayoutForAlphabet()); } - if (oldPreference == null || - oldPreference.isFullscreenMode() != newPreference.isFullscreenMode()) { + if (oldPreference == null + || oldPreference.isFullscreenMode() != newPreference.isFullscreenMode()) { viewManager.setFullscreenMode( - applicationCompatibility.isFullScreenModeSupported() && - newPreference.isFullscreenMode()); + applicationCompatibility.isFullScreenModeSupported() && newPreference.isFullscreenMode()); } - if (oldPreference == null || - oldPreference.getFlickSensitivity() != newPreference.getFlickSensitivity()) { + if (oldPreference == null + || oldPreference.getFlickSensitivity() != newPreference.getFlickSensitivity()) { viewManager.setFlickSensitivity(newPreference.getFlickSensitivity()); } - if (oldPreference == null || - oldPreference.getEmojiProviderType() != newPreference.getEmojiProviderType()) { + if (oldPreference == null + || oldPreference.getEmojiProviderType() != newPreference.getEmojiProviderType()) { viewManager.setEmojiProviderType(newPreference.getEmojiProviderType()); } - if (oldPreference == null || - oldPreference.getHardwareKeyMap() != newPreference.getHardwareKeyMap()) { - hardwareKeyboard.setHardwareKeyMap(newPreference.getHardwareKeyMap()); + if (oldPreference == null + || oldPreference.getHardwareKeyMap() != newPreference.getHardwareKeyMap()) { + viewManager.setHardwareKeyMap(newPreference.getHardwareKeyMap()); } - if (oldPreference == null || - oldPreference.getSkinType() != newPreference.getSkinType()) { - viewManager.setSkinType(newPreference.getSkinType()); + if (oldPreference == null + || oldPreference.getSkinType() != newPreference.getSkinType()) { + viewManager.setSkin(newPreference.getSkinType().getSkin(getResources())); } - if (oldPreference == null || - oldPreference.getLayoutAdjustment() != newPreference.getLayoutAdjustment()) { - viewManager.setLayoutAdjustment(getResources(), newPreference.getLayoutAdjustment()); + if (oldPreference == null + || oldPreference.isMicrophoneButtonEnabled() != newPreference.isMicrophoneButtonEnabled()) { + viewManager.setMicrophoneButtonEnabledByPreference(newPreference.isMicrophoneButtonEnabled()); } - if (oldPreference == null || - oldPreference.getKeyboardHeightRatio() != newPreference.getKeyboardHeightRatio()) { + if (oldPreference == null + || oldPreference.getLayoutAdjustment() != newPreference.getLayoutAdjustment()) { + viewManager.setLayoutAdjustment(newPreference.getLayoutAdjustment()); + } + if (oldPreference == null + || oldPreference.getKeyboardHeightRatio() != newPreference.getKeyboardHeightRatio()) { viewManager.setKeyboardHeightRatio(newPreference.getKeyboardHeightRatio()); } @@ -1455,18 +1563,17 @@ private static int getPreeditLength(Preedit preedit) { * * Some config items should be mobile ones. * For example, "selection shortcut" should be disabled on software keyboard - * regardless of stored config. - * Imposed config should be based on device configuration - * but currently we ignore device config because we currently do not support - * hardware keyboard. - * - * @param deviceConfig the current device configuration + * regardless of stored config if there is no hardware keyboard. */ - private void updateImposedConfig(Configuration deviceConfig) { + private void updateImposedConfig() { + // TODO(hsumita): Respect Config.SelectionShortcut. + SelectionShortcut shortcutMode = (viewManager != null && viewManager.isFloatingCandidateMode()) + ? SelectionShortcut.SHORTCUT_123456789 : SelectionShortcut.NO_SHORTCUT; + // TODO(matsuzakit): deviceConfig should be used to set following config items. sessionExecutor.setImposedConfig(Config.newBuilder() .setSessionKeymap(SessionKeymap.MOBILE) - .setSelectionShortcut(SelectionShortcut.NO_SHORTCUT) + .setSelectionShortcut(shortcutMode) .setUseEmojiConversion(true) .build()); } @@ -1487,20 +1594,21 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin return; } propagateClientSidePreference( - new ClientSidePreference(sharedPreferences, getConfiguration().orientation)); + new ClientSidePreference( + sharedPreferences, getResources(), getConfiguration().orientation)); sessionExecutor.setConfig(ConfigUtil.toConfig(sharedPreferences)); - sessionExecutor.preferenceUsageStatsEvent(sharedPreferences); + sessionExecutor.preferenceUsageStatsEvent(sharedPreferences, getResources()); } } - void maybeSetNarrowMode(Configuration configuration) { + @VisibleForTesting void maybeSetNarrowMode(Configuration configuration) { // If given hardKeyboardHidden is equal to current one, skip updating narrow mode. // In other words, only hardKeyboardHidden flag changes narrow mode automatically. // This behavior is beneficial for a user who want to change narrow/full mode manually - // because this method keeps current narrow mode unless hardwarekeyboard connection is changed. + // because this method keeps current narrow mode unless hardware keyboard connection is changed. if (viewManager != null && configuration.hardKeyboardHidden != currentHardKeyboardHidden) { currentHardKeyboardHidden = configuration.hardKeyboardHidden; - switch(currentHardKeyboardHidden) { + switch (currentHardKeyboardHidden) { case Configuration.HARDKEYBOARDHIDDEN_NO: if (!viewManager.isNarrowMode()) { viewManager.hideSubInputView(); @@ -1518,7 +1626,7 @@ void maybeSetNarrowMode(Configuration configuration) { } } - void onConfigurationChangedInternal(Configuration newConfig) { + @VisibleForTesting void onConfigurationChangedInternal(Configuration newConfig) { InputConnection inputConnection = getCurrentInputConnection(); if (inputConnection != null) { if (inputBound) { @@ -1544,12 +1652,16 @@ void onConfigurationChangedInternal(Configuration newConfig) { resetContext(); selectionTracker.onConfigurationChanged(); + sessionExecutor.updateRequest( + MozcUtil.getRequestBuilder(getResources(), currentKeyboardSpecification, newConfig).build(), + Collections.emptyList()); + // NOTE : This method is not called at the time when the service is started. - // Based on newConfig, imposed config and client side prefereces should be sent + // Based on newConfig, client side preferences should be sent // because they change based on device config. - updateImposedConfig(newConfig); propagateClientSidePreference(new ClientSidePreference( - PreferenceManager.getDefaultSharedPreferences(this), newConfig.orientation)); + Preconditions.checkNotNull(PreferenceManager.getDefaultSharedPreferences(this)), + getResources(), newConfig.orientation)); maybeSetNarrowMode(newConfig); viewManager.onConfigurationChanged(newConfig); } @@ -1562,6 +1674,7 @@ public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); } + @VisibleForTesting void onUpdateSelectionInternal(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { @@ -1643,11 +1756,6 @@ ViewEventListener getViewEventListener() { return eventListener; } - /** - * attachBaseContext is defined with protected visibility in - * {@link android.content.ContextWrapper} but this is required for testing. - * Here just makes it public. - */ @Override @VisibleForTesting public void attachBaseContext(Context base) { diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java b/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java index a65ec446b..761250aee 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java +++ b/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java @@ -29,10 +29,8 @@ package org.mozc.android.inputmethod.japanese; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable; import org.mozc.android.inputmethod.japanese.util.ResourcesWrapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; @@ -182,6 +180,9 @@ public boolean handleMessage(Message msg) { private static Optional isDevChannel = Optional.absent(); private static Optional isMozcEnabled = Optional.absent(); private static Optional isMozcDefaultIme = Optional.absent(); + private static Optional isSystemApplication = Optional.absent(); + private static Optional isTouchUI = Optional.absent(); + private static Optional isUpdatedSystemApplication = Optional.absent(); private static Optional versionCode = Optional.absent(); private static Optional uptimeMillis = Optional.absent(); @@ -250,6 +251,38 @@ public static final boolean isDevChannel(Context context) { return isDevChannelVersionName(getVersionName(context)); } + public static final boolean isSystemApplication(Context context) { + Preconditions.checkNotNull(context); + if (isSystemApplication.isPresent()) { + return isSystemApplication.get(); + } + return checkApplicationFlag(context, ApplicationInfo.FLAG_SYSTEM); + } + + /** + * For testing purpose. + * @param isSystemApplication Optional.absent() if default behavior is preferable + */ + public static final void setSystemApplication(Optional isSystemApplication) { + MozcUtil.isSystemApplication = Preconditions.checkNotNull(isSystemApplication); + } + + public static final boolean isUpdatedSystemApplication(Context context) { + Preconditions.checkNotNull(context); + if (isUpdatedSystemApplication.isPresent()) { + return isUpdatedSystemApplication.get(); + } + return checkApplicationFlag(context, ApplicationInfo.FLAG_UPDATED_SYSTEM_APP); + } + + /** + * For testing purpose. + * @param isUpdatedSystemApplication Optional.absent() if default behavior is preferable + */ + public static final void setUpdatedSystemApplication( + Optional isUpdatedSystemApplication) { + MozcUtil.isUpdatedSystemApplication = Preconditions.checkNotNull(isUpdatedSystemApplication); + } /** * Gets version name. @@ -437,6 +470,26 @@ private static Optional getMozcInputMethodInfo(Context context) return Optional.absent(); } + /** + * Returns true is touch UI should be shown. + */ + public static boolean isTouchUI(Context context) { + Preconditions.checkNotNull(context); + if (isTouchUI.isPresent()) { + return isTouchUI.get(); + } + return context.getResources().getConfiguration().touchscreen + != Configuration.TOUCHSCREEN_NOTOUCH; + } + + /** + * For testing purpose. + * + * @param isTouchUI Optional.absent() if default behavior is preferable + */ + public static final void setTouchUI(Optional isTouchUI) { + MozcUtil.isTouchUI = Preconditions.checkNotNull(isTouchUI); + } public static long getUptimeMillis() { if (uptimeMillis.isPresent()) { @@ -557,69 +610,102 @@ public static void setWindowToken(IBinder token, Dialog dialog) { window.setAttributes(layoutParams); } - public static Request getRequestForKeyboard( - KeyboardSpecificationName specName, - Optional romanjiTable, - Optional spaceOnAlphanumeric, - Optional isKanaModifierInsensitiveConversion, - Optional crossingEdgeBehavior, - Configuration configuration) { - Preconditions.checkNotNull(specName); - Preconditions.checkNotNull(romanjiTable); - Preconditions.checkNotNull(spaceOnAlphanumeric); - Preconditions.checkNotNull(isKanaModifierInsensitiveConversion); - Preconditions.checkNotNull(crossingEdgeBehavior); + private static Request.Builder getRequestBuilderInternal( + KeyboardSpecification specification, Configuration configuration) { + return Request.newBuilder() + .setKeyboardName( + specification.getKeyboardSpecificationName().formattedKeyboardName(configuration)) + .setSpecialRomanjiTable(specification.getSpecialRomanjiTable()) + .setSpaceOnAlphanumeric(specification.getSpaceOnAlphanumeric()) + .setKanaModifierInsensitiveConversion( + specification.isKanaModifierInsensitiveConversion()) + .setCrossingEdgeBehavior(specification.getCrossingEdgeBehavior()); + } + + private static void setHardwareKeyboardRequest(Request.Builder builder, Resources resources) { + builder.setMixedConversion(false) + .setZeroQuerySuggestion(false) + .setUpdateInputModeFromSurroundingText(true) + .setAutoPartialSuggestion(false) + .setCandidatePageSize(resources.getInteger(R.integer.floating_candidate_candidate_num)); + } + + public static void setSoftwareKeyboardRequest(Request.Builder builder) { + builder.setMixedConversion(true) + .setZeroQuerySuggestion(true) + .setUpdateInputModeFromSurroundingText(false) + .setAutoPartialSuggestion(true); + } + + public static Request.Builder getRequestBuilder( + Resources resources, KeyboardSpecification specification, Configuration configuration) { + Preconditions.checkNotNull(resources); + Preconditions.checkNotNull(specification); Preconditions.checkNotNull(configuration); - Request.Builder builder = Request.newBuilder(); - builder.setKeyboardName(specName.formattedKeyboardName(configuration)); - if (romanjiTable.isPresent()) { - builder.setSpecialRomanjiTable(romanjiTable.get()); - } - if (spaceOnAlphanumeric.isPresent()) { - builder.setSpaceOnAlphanumeric(spaceOnAlphanumeric.get()); - } - if (isKanaModifierInsensitiveConversion.isPresent()) { - builder.setKanaModifierInsensitiveConversion( - isKanaModifierInsensitiveConversion.get().booleanValue()); - } - if (crossingEdgeBehavior.isPresent()) { - builder.setCrossingEdgeBehavior(crossingEdgeBehavior.get()); + Request.Builder builder = getRequestBuilderInternal(specification, configuration); + if (specification.isHardwareKeyboard()) { + setHardwareKeyboardRequest(builder, resources); + } else { + setSoftwareKeyboardRequest(builder); } - return builder.build(); + return builder; } @SuppressLint("InlinedApi") - public static boolean isPasswordField(EditorInfo editorInfo) { - Preconditions.checkNotNull(editorInfo); - int inputType = editorInfo.inputType; + public static boolean isPasswordField(int inputType) { int inputClass = inputType & InputType.TYPE_MASK_CLASS; int inputVariation = inputType & InputType.TYPE_MASK_VARIATION; - return inputClass == InputType.TYPE_CLASS_TEXT && - (inputVariation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || - inputVariation == InputType.TYPE_TEXT_VARIATION_PASSWORD || - inputVariation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD); + return inputClass == InputType.TYPE_CLASS_TEXT + && (inputVariation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + || inputVariation == InputType.TYPE_TEXT_VARIATION_PASSWORD + || inputVariation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD); } /** - * Returns true if the editor accepts microphone input. + * Returns true if voice input is preferred by EditorInfo.inputType. * - *

Some editors sends a special record in privateImeOptions. In such situation transition to - * Voice IME should be disabled. + *

Caller should check that voice input is allowed or not by isVoiceInputAllowed(). */ - public static boolean isVoiceInputAllowed(EditorInfo editorInfo) { + public static boolean isVoiceInputPreferred(EditorInfo editorInfo) { Preconditions.checkNotNull(editorInfo); - if (editorInfo.privateImeOptions == null) { - return true; + + // Check privateImeOptions to ensure the text field supports voice input. + if (editorInfo.privateImeOptions != null) { + for (String option : editorInfo.privateImeOptions.split(",")) { + if (option.equals(IME_OPTION_NO_MICROPHONE) + || option.equals(IME_OPTION_NO_MICROPHONE_COMPAT)) { + return false; + } + } } - for (String option : editorInfo.privateImeOptions.split(",")) { - if (option.equals(IME_OPTION_NO_MICROPHONE) || - option.equals(IME_OPTION_NO_MICROPHONE_COMPAT)) { - return false; + + int inputType = editorInfo.inputType; + if (isNumberKeyboardPreferred(inputType) || isPasswordField(inputType)) { + return false; + } + if ((inputType & EditorInfo.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { + switch (inputType & EditorInfo.TYPE_MASK_VARIATION) { + case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + case InputType.TYPE_TEXT_VARIATION_URI: + return false; + default: + break; } } return true; } + /** Returns true if number keyboard is preferred by EditorInfo.inputType. */ + public static boolean isNumberKeyboardPreferred(int inputType) { + int typeClass = inputType & InputType.TYPE_MASK_CLASS; + // As of API Level 21, following condition equals to "typeClass != InputType.TYPE_CLASS_TEXT". + // However type-class might be added in future so safer expression is employed here. + return typeClass == InputType.TYPE_CLASS_DATETIME + || typeClass == InputType.TYPE_CLASS_NUMBER + || typeClass == InputType.TYPE_CLASS_PHONE; + } + public static String utf8CStyleByteStringToString(ByteString value) { Preconditions.checkNotNull(value); // Find '\0' terminator. (if value doesn't contain '\0', the size should be as same as diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcView.java b/src/android/src/com/google/android/inputmethod/japanese/MozcView.java index 5bce6c6b7..e59f11935 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/MozcView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/MozcView.java @@ -29,28 +29,33 @@ package org.mozc.android.inputmethod.japanese; +import org.mozc.android.inputmethod.japanese.CandidateViewManager.KeyboardCandidateViewHeightListener; import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; import org.mozc.android.inputmethod.japanese.LayoutParamsAnimator.InterpolationListener; import org.mozc.android.inputmethod.japanese.ViewManagerInterface.LayoutAdjustment; import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; -import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory; import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler; import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; +import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView; import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage; +import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; -import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output; import org.mozc.android.inputmethod.japanese.ui.SideFrameStubProxy; -import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; -import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.MozcImageView; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.inputmethodservice.InputMethodService.Insets; import android.os.Handler; @@ -63,48 +68,50 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.animation.AnimationSet; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.TranslateAnimation; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.widget.CompoundButton; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.LinearLayout; import java.util.Collections; import java.util.EnumSet; +import javax.annotation.Nullable; + /** * Root {@code View} of the MechaMozc. * It is expected that instance methods are used after inflation is done. * */ -public class MozcView extends LinearLayout implements MemoryManageable { +public class MozcView extends FrameLayout implements MemoryManageable { + @VisibleForTesting static class DimensionPixelSize { final int imeWindowPartialWidth; final int imeWindowRegionInsetThreshold; final int narrowFrameHeight; final int narrowImeWindowHeight; final int sideFrameWidth; - final int translucentBorderHeight; + final int buttonFrameHeight; public DimensionPixelSize(Resources resources) { imeWindowPartialWidth = resources.getDimensionPixelSize(R.dimen.ime_window_partial_width); imeWindowRegionInsetThreshold = resources.getDimensionPixelSize( R.dimen.ime_window_region_inset_threshold); narrowFrameHeight = resources.getDimensionPixelSize(R.dimen.narrow_frame_height); narrowImeWindowHeight = resources.getDimensionPixelSize(R.dimen.narrow_ime_window_height); - translucentBorderHeight = resources.getDimensionPixelSize( - R.dimen.translucent_border_height); sideFrameWidth = resources.getDimensionPixelSize(R.dimen.side_frame_width); + buttonFrameHeight = resources.getDimensionPixelSize(R.dimen.button_frame_height); } } + @VisibleForTesting static class HeightLinearInterpolationListener implements InterpolationListener { - @VisibleForTesting final int fromHeight; - @VisibleForTesting final int toHeight; + final int fromHeight; + final int toHeight; public HeightLinearInterpolationListener(int fromHeight, int toHeight) { this.fromHeight = fromHeight; @@ -121,20 +128,20 @@ public ViewGroup.LayoutParams calculateAnimatedParams( // TODO(hidehiko): Refactor CandidateViewListener along with View structure refactoring. class InputFrameFoldButtonClickListener implements OnClickListener { - private final ViewEventListener eventListener; private final View keyboardView; + private final int originalHeight; private final long foldDuration; private final Interpolator foldKeyboardViewInterpolator; private final long expandDuration; private final Interpolator expandKeyboardViewInterpolator; private final LayoutParamsAnimator layoutParamsAnimator; InputFrameFoldButtonClickListener( - ViewEventListener eventListener, View keyboardView, + View keyboardView, int originalHeight, long foldDuration, Interpolator foldKeyboardViewInterpolator, long expandDuration, Interpolator expandKeyboardViewInterpolator, LayoutParamsAnimator layoutParamsAnimator) { - this.eventListener = eventListener; this.keyboardView = keyboardView; + this.originalHeight = originalHeight; this.foldDuration = foldDuration; this.foldKeyboardViewInterpolator = foldKeyboardViewInterpolator; this.expandDuration = expandDuration; @@ -144,30 +151,45 @@ class InputFrameFoldButtonClickListener implements OnClickListener { @Override public void onClick(View v) { - if (keyboardView.getHeight() == getInputFrameHeight()) { - eventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_FOLD); + if (keyboardView.getHeight() == originalHeight) { + if (viewEventListener != null) { + viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_FOLD); + } layoutParamsAnimator.startAnimation( keyboardView, new HeightLinearInterpolationListener(keyboardView.getHeight(), 0), foldKeyboardViewInterpolator, foldDuration, 0); CompoundButton.class.cast(v).setChecked(true); } else { - eventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND); + if (viewEventListener != null) { + viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND); + } layoutParamsAnimator.startAnimation( keyboardView, - new HeightLinearInterpolationListener(keyboardView.getHeight(), getInputFrameHeight()), + new HeightLinearInterpolationListener(keyboardView.getHeight(), originalHeight), expandKeyboardViewInterpolator, expandDuration, 0); CompoundButton.class.cast(v).setChecked(false); } } } - // TODO(hidehiko): Move hard coded parameters to dimens.xml or skin. - private static final float NARROW_MODE_BUTTON_CORNOR_RADIUS = 3.5f; // in dip. - private static final float NARROW_MODE_BUTTON_LEFT_OFFSET = 2.0f; - private static final float NARROW_MODE_BUTTON_TOP_OFFSET = 1.0f; - private static final float NARROW_MODE_BUTTON_RIGHT_OFFSET = 2.0f; - private static final float NARROW_MODE_BUTTON_BOTTOM_OFFSET = 3.0f; + /** Manages background view height. */ + @VisibleForTesting + class SoftwareKeyboardHeightListener implements KeyboardCandidateViewHeightListener { + @Override + public void onExpanded() { + if (!isNarrowMode()) { + changeBottomBackgroundHeight(imeWindowHeight); + } + } + + @Override + public void onCollapse() { + if (!isNarrowMode() && getSymbolInputView().getVisibility() != VISIBLE) { + resetBottomBackgroundHeight(); + } + } + } @VisibleForTesting final InOutAnimatedFrameLayout.VisibilityChangeListener onVisibilityChangeListener = @@ -181,23 +203,22 @@ public void onVisibilityChange() { private final DimensionPixelSize dimensionPixelSize = new DimensionPixelSize(getResources()); private final SideFrameStubProxy leftFrameStubProxy = new SideFrameStubProxy(); private final SideFrameStubProxy rightFrameStubProxy = new SideFrameStubProxy(); - private final MozcDrawableFactory mozcDrawableFactory = new MozcDrawableFactory(getResources()); + @VisibleForTesting ViewEventListener viewEventListener; @VisibleForTesting boolean fullscreenMode = false; - boolean narrowMode = false; - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; + @VisibleForTesting boolean narrowMode = false; + private boolean buttonFrameVisible = true; + private Skin skin = Skin.getFallbackInstance(); @VisibleForTesting LayoutAdjustment layoutAdjustment = LayoutAdjustment.FILL; private int inputFrameHeight = 0; @VisibleForTesting int imeWindowHeight = 0; - @VisibleForTesting Animation candidateViewInAnimation; - @VisibleForTesting Animation candidateViewOutAnimation; + @VisibleForTesting int symbolInputViewHeight = 0; @VisibleForTesting Animation symbolInputViewInAnimation; @VisibleForTesting Animation symbolInputViewOutAnimation; - @VisibleForTesting Animation dropShadowCandidateViewInAnimation; - @VisibleForTesting Animation dropShadowCandidateViewOutAnimation; - @VisibleForTesting Animation dropShadowSymbolInputViewInAnimation; - @VisibleForTesting Animation dropShadowSymbolInputViewOutAnimation; - @VisibleForTesting boolean isDropShadowExpanded = false; + @VisibleForTesting SoftwareKeyboardHeightListener softwareKeyboardHeightListener = + new SoftwareKeyboardHeightListener(); + @VisibleForTesting CandidateViewManager candidateViewManager; + @VisibleForTesting boolean allowFloatingCandidateMode; public MozcView(Context context) { super(context); @@ -207,82 +228,29 @@ public MozcView(Context context, AttributeSet attrSet) { super(context, attrSet); } - private static Drawable createButtonBackgroundDrawable(float density) { - return BackgroundDrawableFactory.createPressableDrawable( - new RoundRectKeyDrawable( - (int) (NARROW_MODE_BUTTON_LEFT_OFFSET * density), - (int) (NARROW_MODE_BUTTON_TOP_OFFSET * density), - (int) (NARROW_MODE_BUTTON_RIGHT_OFFSET * density), - (int) (NARROW_MODE_BUTTON_BOTTOM_OFFSET * density), - (int) (NARROW_MODE_BUTTON_CORNOR_RADIUS * density), - 0xFFE9E4E4, 0xFFB2ADAD, 0, 0xFF1E1E1E), - new RoundRectKeyDrawable( - (int) (NARROW_MODE_BUTTON_LEFT_OFFSET * density), - (int) (NARROW_MODE_BUTTON_TOP_OFFSET * density), - (int) (NARROW_MODE_BUTTON_RIGHT_OFFSET * density), - (int) (NARROW_MODE_BUTTON_BOTTOM_OFFSET * density), - (int) (NARROW_MODE_BUTTON_CORNOR_RADIUS * density), - 0xFF858087, 0xFF67645F, 0, 0xFF1E1E1E)); - } - - @SuppressWarnings("deprecation") - private void setupImageButton(ImageView view, int resourceID) { - float density = getResources().getDisplayMetrics().density; - view.setImageDrawable(mozcDrawableFactory.getDrawable(resourceID).orNull()); - view.setBackgroundDrawable(createButtonBackgroundDrawable(density)); - view.setPadding(0, 0, 0, 0); - } - private static Animation createAlphaAnimation(float fromAlpha, float toAlpha, long duration) { AlphaAnimation animation = new AlphaAnimation(fromAlpha, toAlpha); animation.setDuration(duration); return animation; } - private static Animation createCandidateViewTransitionAnimation(int fromY, int toY, - float fromAlpha, float toAlpha, - long duration) { - AnimationSet animation = new AnimationSet(false); - animation.setDuration(duration); - - AlphaAnimation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha); - alphaAnimation.setDuration(duration); - animation.addAnimation(alphaAnimation); - - TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, fromY, toY); - translateAnimation.setInterpolator(new DecelerateInterpolator()); - translateAnimation.setDuration(duration); - animation.addAnimation(translateAnimation); - return animation; - } - @Override public void onFinishInflate() { setKeyboardHeightRatio(100); - setupImageButton(getWidenButton(), R.raw.hardware__function__close); - setupImageButton(getHardwareCompositionButton(), R.raw.qwerty__function__kana__icon); - leftFrameStubProxy.initialize(this, - R.id.stub_left_frame, R.id.dropshadow_left_short_top, - R.id.dropshadow_left_long_top, R.id.left_adjust_button, - R.raw.adjust_arrow_left, 1.0f, R.id.left_dropshadow_short, - R.id.left_dropshadow_long); + R.id.stub_left_frame, R.id.left_adjust_button, + R.raw.adjust_arrow_left); rightFrameStubProxy.initialize(this, - R.id.stub_right_frame, R.id.dropshadow_right_short_top, - R.id.dropshadow_right_long_top, R.id.right_adjust_button, - R.raw.adjust_arrow_right, 0.0f, R.id.right_dropshadow_short, - R.id.right_dropshadow_long); - } + R.id.stub_right_frame, R.id.right_adjust_button, + R.raw.adjust_arrow_right); - public void setEventListener(final ViewEventListener viewEventListener, - OnClickListener widenButtonClickListener, - OnClickListener leftAdjustButtonClickListener, - OnClickListener rightAdjustButtonClickListener) { - checkInflated(); + candidateViewManager = new CandidateViewManager( + getKeyboardCandidateView(), + FloatingCandidateView.class.cast(findViewById(R.id.floating_candidate_view))); + } - // Propagate the given listener into the child views. - // Set CandidateViewListener as well here, because it uses viewEventListener. + private InputFrameFoldButtonClickListener createFoldButtonListener(View view, int height) { Resources resources = getResources(); int foldOvershootDurationRate = resources.getInteger(R.integer.input_frame_fold_overshoot_duration_rate); @@ -292,50 +260,60 @@ public void setEventListener(final ViewEventListener viewEventListener, resources.getInteger(R.integer.input_frame_expand_overshoot_duration_rate); int expandOvershootRate = resources.getInteger(R.integer.input_frame_expand_overshoot_rate); - getCandidateView().setViewEventListener( - viewEventListener, - new InputFrameFoldButtonClickListener( - viewEventListener, getKeyboardFrame(), - resources.getInteger(R.integer.input_frame_fold_duration), - SequentialInterpolator.newBuilder() - .add(new DecelerateInterpolator(), - foldOvershootDurationRate, -foldOvershootRate / 1e6f) - .add(new AccelerateInterpolator(), 1e6f - foldOvershootDurationRate, 1) - .build(), - resources.getInteger(R.integer.input_frame_expand_duration), - SequentialInterpolator.newBuilder() - .add(new DecelerateInterpolator(), - expandOvershootDurationRate, 1 + expandOvershootRate / 1e6f) - .add(new AccelerateDecelerateInterpolator(), 1e6f - expandOvershootDurationRate, 1) - .build(), - new LayoutParamsAnimator(new Handler(Looper.myLooper())))); - - getSymbolInputView().setViewEventListener( + + return new InputFrameFoldButtonClickListener( + view, height, resources.getInteger(R.integer.input_frame_fold_duration), + SequentialInterpolator.newBuilder() + .add(new DecelerateInterpolator(), + foldOvershootDurationRate, -foldOvershootRate / 1e6f) + .add(new AccelerateInterpolator(), 1e6f - foldOvershootDurationRate, 1) + .build(), + resources.getInteger(R.integer.input_frame_expand_duration), + SequentialInterpolator.newBuilder() + .add(new DecelerateInterpolator(), + expandOvershootDurationRate, 1 + expandOvershootRate / 1e6f) + .add(new AccelerateDecelerateInterpolator(), 1e6f - expandOvershootDurationRate, 1) + .build(), + new LayoutParamsAnimator(new Handler(Looper.myLooper()))); + } + + public void setEventListener(final ViewEventListener viewEventListener, + OnClickListener widenButtonClickListener, + OnClickListener leftAdjustButtonClickListener, + OnClickListener rightAdjustButtonClickListener, + OnClickListener microphoneButtonClickListener) { + Preconditions.checkNotNull(viewEventListener); + Preconditions.checkNotNull(widenButtonClickListener); + Preconditions.checkNotNull(leftAdjustButtonClickListener); + Preconditions.checkNotNull(rightAdjustButtonClickListener); + Preconditions.checkNotNull(microphoneButtonClickListener); + + checkInflated(); + + this.viewEventListener = viewEventListener; + + // Propagate the given listener into the child views. + // Set CandidateViewListener as well here, because it uses viewEventListener. + candidateViewManager.setEventListener(viewEventListener, softwareKeyboardHeightListener); + + getSymbolInputView().setEventListener( viewEventListener, - /** - * Click handler of the close button. - */ + /** Click handler of the close button. */ new OnClickListener() { @Override public void onClick(View v) { if (viewEventListener != null) { - viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_FOLD); + viewEventListener.onFireFeedbackEvent(FeedbackEvent.SYMBOL_INPUTVIEW_CLOSED); } - startSymbolInputViewOutAnimation(); + hideSymbolInputView(); } - }); - - getHardwareCompositionButton().setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - viewEventListener.onHardwareKeyboardCompositionModeChange(CompositionSwitchMode.TOGGLE); - } - }); - - getWidenButton().setOnClickListener(widenButtonClickListener); + }, + microphoneButtonClickListener); + getNarrowFrame().setEventListener(viewEventListener, widenButtonClickListener); leftFrameStubProxy.setButtonOnClickListener(leftAdjustButtonClickListener); rightFrameStubProxy.setButtonOnClickListener(rightAdjustButtonClickListener); + getMicrophoneButton().setOnClickListener(microphoneButtonClickListener); } public void setKeyEventHandler(KeyEventHandler keyEventHandler) { @@ -347,14 +325,17 @@ public void setKeyEventHandler(KeyEventHandler keyEventHandler) { } // TODO(hidehiko): Probably we'd like to remove this method when we decide to move MVC model. - public JapaneseKeyboard getJapaneseKeyboard() { + @Nullable public Keyboard getKeyboard() { checkInflated(); - return getKeyboardView().getJapaneseKeyboard(); + return getKeyboardView().getKeyboard().orNull(); } - public void setJapaneseKeyboard(JapaneseKeyboard keyboard) { + public void setKeyboard(Keyboard keyboard) { checkInflated(); - getKeyboardView().setJapaneseKeyboard(keyboard); + getKeyboardView().setKeyboard(keyboard); + CompositionMode compositionMode = keyboard.getSpecification().getCompositionMode(); + getNarrowFrame().setHardwareCompositionButtonImage(compositionMode); + candidateViewManager.setHardwareCompositionMode(compositionMode); } public void setEmojiEnabled(boolean unicodeEmojiEnabled, boolean carrierEmojiEnabled) { @@ -371,6 +352,7 @@ public void setPasswordField(boolean isPasswordField) { public void setEditorInfo(EditorInfo editorInfo) { checkInflated(); getKeyboardView().setEditorInfo(editorInfo); + candidateViewManager.setEditorInfo(editorInfo); } public void setFlickSensitivity(int flickSensitivity) { @@ -393,6 +375,7 @@ public void setSymbolCandidateStorage(SymbolCandidateStorage symbolCandidateStor public void setPopupEnabled(boolean popupEnabled) { checkInflated(); getKeyboardView().setPopupEnabled(popupEnabled); + getSymbolInputView().setPopupEnabled(popupEnabled); } public boolean isPopupEnabled() { @@ -400,16 +383,28 @@ public boolean isPopupEnabled() { return getKeyboardView().isPopupEnabled(); } - public void setSkinType(SkinType skinType) { + @SuppressWarnings("deprecation") + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); checkInflated(); - this.skinType = skinType; - getKeyboardView().setSkinType(skinType); - getSymbolInputView().setSkinType(skinType); - getCandidateView().setSkinType(skinType); - } - - public SkinType getSkinType() { - return skinType; + this.skin = skin; + getKeyboardView().setSkin(skin); + getSymbolInputView().setSkin(skin); + candidateViewManager.setSkin(skin); + getMicrophoneButton().setBackgroundDrawable(BackgroundDrawableFactory.createPressableDrawable( + new ColorDrawable(skin.buttonFrameButtonPressedColor), Optional.absent())); + getMicrophoneButton().setSkin(skin); + leftFrameStubProxy.setSkin(skin); + rightFrameStubProxy.setSkin(skin); + getButtonFrame().setBackgroundDrawable( + skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable()); + getNarrowFrame().setSkin(skin); + getKeyboardFrameSeparator().setBackgroundDrawable( + skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable()); + } + + public Skin getSkin() { + return skin; } /** @@ -417,6 +412,7 @@ public SkinType getSkinType() { * or do nothing otherwise. * Exposed as a package private method for testing purpose. */ + @VisibleForTesting void checkInflated() { if (getChildCount() == 0) { throw new IllegalStateException("It is necessary to inflate mozc_view.xml"); @@ -425,22 +421,36 @@ void checkInflated() { public void setCommand(Command outCommand) { checkInflated(); + candidateViewManager.update(outCommand); + updateMetaStatesBasedOnOutput(outCommand.getOutput()); + } - CandidateView candidateView = getCandidateView(); - if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() > 0) { - // Call CandidateView#update only if there are some candidates in the output. - // In such case the candidate view will clear its canvas. - candidateView.update(outCommand); - startCandidateViewInAnimation(); - + // Update COMPOSING metastate. + @VisibleForTesting + void updateMetaStatesBasedOnOutput(Output output) { + Preconditions.checkNotNull(output); + + boolean hasPreedit = output.hasPreedit() + && output.getPreedit().getSegmentCount() > 0; + if (hasPreedit) { + getKeyboardView().updateMetaStates(EnumSet.of(MetaState.COMPOSING), + Collections.emptySet()); } else { - // We don't call update method here, because it will clear the view's contents during the - // animation. - // TODO(hidehiko): Clear the candidates when the animation is finished. - startCandidateViewOutAnimation(); + getKeyboardView().updateMetaStates(Collections.emptySet(), + EnumSet.of(MetaState.COMPOSING)); } } + @TargetApi(21) + public void setCursorAnchorInfo(CursorAnchorInfo info) { + candidateViewManager.setCursorAnchorInfo(info); + } + + public void setCursorAnchorInfoEnabled(boolean enabled) { + allowFloatingCandidateMode = enabled; + candidateViewManager.setAllowFloatingMode(enabled); + } + public void reset() { checkInflated(); @@ -449,24 +459,21 @@ public void reset() { resetKeyboardViewState(); // Reset candidate view. - CandidateView candidateView = getCandidateView(); - candidateView.clearAnimation(); - candidateView.setVisibility(View.GONE); - candidateView.reset(); + candidateViewManager.reset(); // Reset symbol input view visibility. Set Visibility directly (without animation). SymbolInputView symbolInputView = getSymbolInputView(); symbolInputView.clearAnimation(); symbolInputView.setVisibility(View.GONE); - // Reset *all* metastates. + // Reset *all* metastates (and set NO_GLOBE as default value). // Expecting metastates will be set next initialization. - getKeyboardView().updateMetaStates(Collections.emptySet(), + getKeyboardView().updateMetaStates(EnumSet.of(MetaState.NO_GLOBE), EnumSet.allOf(MetaState.class)); resetFullscreenMode(); setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode); - collapseDropShadowAndBackground(); + resetBottomBackgroundHeight(); updateBackgroundColor(); } @@ -477,29 +484,38 @@ public void resetKeyboardFrameVisibility() { return; } - View keyboardFrame = getKeyboardFrame(); + SymbolInputView symbolInputView = getSymbolInputView(); + View keyboardFrame; + int keyboardFrameHeight; + if (symbolInputView.isInflated() && symbolInputView.getVisibility() == View.VISIBLE) { + keyboardFrame = getNumberKeyboardFrame(); + keyboardFrameHeight = symbolInputView.getNumberKeyboardHeight(); + } else { + keyboardFrame = getKeyboardFrame(); + keyboardFrameHeight = getInputFrameHeight(); + } + keyboardFrame.setVisibility(View.VISIBLE); // The height may be changed so reset it here. ViewGroup.LayoutParams layoutParams = keyboardFrame.getLayoutParams(); - int keyboardFrameHeight = getInputFrameHeight(); if (layoutParams.height != keyboardFrameHeight) { layoutParams.height = keyboardFrameHeight; keyboardFrame.setLayoutParams(layoutParams); // Also reset the state of the folding button, which is "conceptually" a part of // the keyboard. - getCandidateView().setInputFrameFoldButtonChecked(false); + candidateViewManager.setInputFrameFoldButtonChecked(false); } } public void resetKeyboardViewState() { checkInflated(); - getKeyboardView().resetState(); } - public boolean showSymbolInputView() { + public boolean showSymbolInputView(Optional category) { + Preconditions.checkNotNull(category); checkInflated(); SymbolInputView view = getSymbolInputView(); @@ -509,10 +525,17 @@ public boolean showSymbolInputView() { if (!view.isInflated()) { view.inflateSelf(); + CandidateView numberCandidateView = + CandidateView.class.cast(view.findViewById(R.id.candidate_view_in_symbol_view)); + numberCandidateView.setInputFrameFoldButtonOnClickListener(createFoldButtonListener( + getNumberKeyboardFrame(), view.getNumberKeyboardHeight())); + candidateViewManager.setNumberCandidateView(numberCandidateView); } - view.reset(); + view.resetToMajorCategory(category); startSymbolInputViewInAnimation(); + candidateViewManager.setNumberMode(true); + return true; } @@ -524,41 +547,51 @@ public boolean hideSymbolInputView() { return false; } + candidateViewManager.setNumberMode(false); startSymbolInputViewOutAnimation(); return true; } + private int getButtonFrameHeightIfVisible() { + return buttonFrameVisible ? dimensionPixelSize.buttonFrameHeight : 0; + } + /** * Decides input frame height in not fullscreen mode. */ - public int getVisibleViewHeight() { + @VisibleForTesting + int getVisibleViewHeight() { checkInflated(); + boolean isSymbolInputViewVisible = getSymbolInputView().getVisibility() == View.VISIBLE; // Means only software keyboard or narrow frame - boolean isDefaultView = getCandidateView().getVisibility() != View.VISIBLE - && getSymbolInputView().getVisibility() != View.VISIBLE; + boolean isDefaultView = + !candidateViewManager.isKeyboardCandidateViewVisible() && !isSymbolInputViewVisible; if (narrowMode) { if (isDefaultView) { return dimensionPixelSize.narrowFrameHeight; } else { - return dimensionPixelSize.narrowImeWindowHeight - - dimensionPixelSize.translucentBorderHeight; + return dimensionPixelSize.narrowImeWindowHeight; } } else { if (isDefaultView) { - return getInputFrameHeight(); + return getInputFrameHeight() + getButtonFrameHeightIfVisible(); } else { - return imeWindowHeight - dimensionPixelSize.translucentBorderHeight; + if (isSymbolInputViewVisible) { + return symbolInputViewHeight; + } else { + return imeWindowHeight; + } } } } + @VisibleForTesting void updateInputFrameHeight() { // input_frame's height depends on fullscreen mode, narrow mode and Candidate/Symbol views. if (fullscreenMode) { - setLayoutHeight(getBottomFrame(), getVisibleViewHeight() - + dimensionPixelSize.translucentBorderHeight); + setLayoutHeight(getBottomFrame(), getVisibleViewHeight()); setLayoutHeight(getKeyboardFrame(), getInputFrameHeight()); } else { if (narrowMode) { @@ -570,6 +603,7 @@ void updateInputFrameHeight() { } } + @VisibleForTesting int getSideAdjustedWidth() { return dimensionPixelSize.imeWindowPartialWidth + dimensionPixelSize.sideFrameWidth; } @@ -582,27 +616,29 @@ public boolean isFullscreenMode() { return fullscreenMode; } + @VisibleForTesting void resetFullscreenMode() { if (fullscreenMode) { // In fullscreen mode, InputMethodService shows extract view which height is 0 and // weight is 0. So our MozcView height should be fixed. // If CandidateView or SymbolInputView appears, MozcView height is enlarged to fix them. - setLayoutHeight(getOverlayView(), 0); + getOverlayView().setVisibility(View.GONE); setLayoutHeight(getTextInputFrame(), LayoutParams.WRAP_CONTENT); - getCandidateView().setOnVisibilityChangeListener(onVisibilityChangeListener); + candidateViewManager.setOnVisibilityChangeListener(Optional.of(onVisibilityChangeListener)); getSymbolInputView().setOnVisibilityChangeListener(onVisibilityChangeListener); } else { - setLayoutHeight(getOverlayView(), LayoutParams.MATCH_PARENT); + getOverlayView().setVisibility(View.VISIBLE); setLayoutHeight(getTextInputFrame(), LayoutParams.MATCH_PARENT); - getCandidateView().setOnVisibilityChangeListener(null); + candidateViewManager.setOnVisibilityChangeListener( + Optional.absent()); getSymbolInputView().setOnVisibilityChangeListener(null); } - + candidateViewManager.setExtractedMode(fullscreenMode); updateInputFrameHeight(); updateBackgroundColor(); } - static void setLayoutHeight(View view, int height) { + private static void setLayoutHeight(View view, int height) { ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); layoutParams.height = height; view.setLayoutParams(layoutParams); @@ -612,17 +648,8 @@ public boolean isNarrowMode() { return narrowMode; } - public void setHardwareCompositionButtonImage(CompositionMode compositionMode) { - switch (compositionMode) { - case HIRAGANA: - getHardwareCompositionButton().setImageDrawable( - mozcDrawableFactory.getDrawable(R.raw.qwerty__function__kana__icon).orNull()); - break; - default: - getHardwareCompositionButton().setImageDrawable( - mozcDrawableFactory.getDrawable(R.raw.qwerty__function__alphabet__icon).orNull()); - break; - } + public boolean isFloatingCandidateMode() { + return candidateViewManager.isFloatingMode(); } public Rect getKeyboardSize() { @@ -678,17 +705,18 @@ public void setLayoutAdjustmentAndNarrowMode(LayoutAdjustment layoutAdjustment, float descriptionTextSize = layoutAdjustment == LayoutAdjustment.FILL ? resources.getDimension(R.dimen.candidate_description_text_size) : resources.getDimension(R.dimen.candidate_description_text_size_aligned_layout); - getCandidateView().setCandidateTextDimension(candidateTextSize, descriptionTextSize); + candidateViewManager.setCandidateTextDimension(candidateTextSize, descriptionTextSize); getSymbolInputView().setCandidateTextDimension(candidateTextSize, descriptionTextSize); - getConversionCandidateWordContainerView().setCandidateTextDimension(candidateTextSize); // In narrow mode, hide software keyboard and show narrow status bar. - getCandidateView().setNarrowMode(narrowMode); + candidateViewManager.setNarrowMode(narrowMode); if (narrowMode) { getKeyboardFrame().setVisibility(GONE); + getButtonFrame().setVisibility(GONE); getNarrowFrame().setVisibility(VISIBLE); } else { getKeyboardFrame().setVisibility(VISIBLE); + getButtonFrame().setVisibility(buttonFrameVisible ? VISIBLE : GONE); getNarrowFrame().setVisibility(GONE); resetKeyboardFrameVisibility(); } @@ -709,12 +737,13 @@ public void startLayoutAdjustmentAnimation() { getForegroundFrame().startAnimation(translateAnimation); } + @VisibleForTesting void updateBackgroundColor() { // If fullscreenMode, background should not show original window. // If narrowMode, it is always full-width. // If isFloatingMode, background should be transparent. - int resourceId = (fullscreenMode || (!narrowMode && !isFloatingMode())) ? - R.color.input_frame_background : 0; + int resourceId = (fullscreenMode || (!narrowMode && !isFloatingMode())) + ? R.color.input_frame_background : 0; getBottomBackground().setBackgroundResource(resourceId); } @@ -748,62 +777,29 @@ public void setInsets(int contentViewWidth, int contentViewHeight, Insets outIns return; } - void expandDropShadowAndBackground() { - leftFrameStubProxy.flipDropShadowVisibility(INVISIBLE); - rightFrameStubProxy.flipDropShadowVisibility(INVISIBLE); - getDropShadowTop().setVisibility(VISIBLE); - getResources(); - setLayoutHeight(getBottomBackground(), imeWindowHeight - - (fullscreenMode ? 0 : dimensionPixelSize.translucentBorderHeight)); - isDropShadowExpanded = true; - } - - void collapseDropShadowAndBackground() { - leftFrameStubProxy.flipDropShadowVisibility(VISIBLE); - rightFrameStubProxy.flipDropShadowVisibility(VISIBLE); - getDropShadowTop().setVisibility(fullscreenMode ? VISIBLE : INVISIBLE); - getResources(); - setLayoutHeight(getBottomBackground(), getInputFrameHeight() - + (fullscreenMode ? dimensionPixelSize.translucentBorderHeight : 0)); - isDropShadowExpanded = false; - } - - void startDropShadowAnimation(Animation mainAnimation, Animation subAnimation) { - leftFrameStubProxy.startDropShadowAnimation(subAnimation, mainAnimation); - rightFrameStubProxy.startDropShadowAnimation(subAnimation, mainAnimation); - getDropShadowTop().startAnimation(mainAnimation); - } - - void startCandidateViewInAnimation() { - getCandidateView().startInAnimation(); - if (!isDropShadowExpanded) { - expandDropShadowAndBackground(); - startDropShadowAnimation(candidateViewInAnimation, dropShadowCandidateViewInAnimation); + @VisibleForTesting + void changeBottomBackgroundHeight(int targetHeight) { + if (getBottomBackground().getHeight() != targetHeight) { + setLayoutHeight(getBottomBackground(), targetHeight); } } - void startCandidateViewOutAnimation() { - getCandidateView().startOutAnimation(); - if (getSymbolInputView().getVisibility() != VISIBLE && isDropShadowExpanded) { - collapseDropShadowAndBackground(); - startDropShadowAnimation(candidateViewOutAnimation, dropShadowCandidateViewOutAnimation); - } + @VisibleForTesting + void resetBottomBackgroundHeight() { + setLayoutHeight(getBottomBackground(), getInputFrameHeight() + getButtonFrameHeightIfVisible()); } + @VisibleForTesting void startSymbolInputViewInAnimation() { getSymbolInputView().startInAnimation(); - if (!isDropShadowExpanded) { - expandDropShadowAndBackground(); - startDropShadowAnimation(symbolInputViewInAnimation, dropShadowSymbolInputViewInAnimation); - } + changeBottomBackgroundHeight(symbolInputViewHeight); } + @VisibleForTesting void startSymbolInputViewOutAnimation() { getSymbolInputView().startOutAnimation(); - if (getCandidateView().getVisibility() != VISIBLE && isDropShadowExpanded) { - collapseDropShadowAndBackground(); - startDropShadowAnimation(symbolInputViewOutAnimation, - dropShadowSymbolInputViewOutAnimation); + if (!candidateViewManager.isKeyboardCandidateViewVisible()) { + resetBottomBackgroundHeight(); } } @@ -811,52 +807,34 @@ void startSymbolInputViewOutAnimation() { * Reset components depending inputFrameHeight or imeWindowHeight. * This should be called when inputFrameHeight and/or imeWindowHeight are updated. */ + @VisibleForTesting void resetHeightDependingComponents() { - // Create In/Out animation which dropshadows share between CandidateView and SymbolInputView. - { - CandidateView candidateView = getCandidateView(); - int windowHeight = imeWindowHeight; - int inputFrameHeight = getInputFrameHeight(); - int candidateViewHeight = windowHeight - inputFrameHeight; - long duration = getResources().getInteger(R.integer.candidate_frame_transition_duration); - float fromAlpha = 0.0f; - float toAlpha = 1.0f; + getKeyboardCandidateView().setInputFrameFoldButtonOnClickListener( + createFoldButtonListener(getKeyboardFrame(), getInputFrameHeight())); - candidateViewInAnimation = createCandidateViewTransitionAnimation( - candidateViewHeight, 0, fromAlpha, toAlpha, duration); - candidateView.setInAnimation(candidateViewInAnimation); - dropShadowCandidateViewInAnimation = createAlphaAnimation( - 1.0f - fromAlpha, 1.0f - toAlpha, duration); - - candidateViewOutAnimation = createCandidateViewTransitionAnimation( - 0, candidateViewHeight, toAlpha, fromAlpha, duration); - candidateView.setOutAnimation(candidateViewOutAnimation); - dropShadowCandidateViewOutAnimation = createAlphaAnimation( - 1.0f - toAlpha, 1.0f - fromAlpha, duration); + if (candidateViewManager != null) { + candidateViewManager.resetHeightDependingComponents( + getResources(), imeWindowHeight, inputFrameHeight); } SymbolInputView symbolInputView = getSymbolInputView(); { - long duration = getResources().getInteger(R.integer.symbol_input_transition_duration_in); + long duration = getResources().getInteger(R.integer.symbol_input_transition_duration); float fromAlpha = 0.3f; float toAlpha = 1.0f; symbolInputViewInAnimation = createAlphaAnimation(fromAlpha, toAlpha, duration); symbolInputView.setInAnimation(symbolInputViewInAnimation); - dropShadowSymbolInputViewInAnimation = createAlphaAnimation( - 1.0f - fromAlpha, 1.0f - toAlpha, duration); - symbolInputViewOutAnimation = createAlphaAnimation(toAlpha, fromAlpha, duration); symbolInputView.setOutAnimation(symbolInputViewOutAnimation); - dropShadowSymbolInputViewOutAnimation = createAlphaAnimation( - 1.0f - toAlpha, 1.0f - fromAlpha, duration); } - // Reset drop shadow height. - int shortHeight = getInputFrameHeight() + dimensionPixelSize.translucentBorderHeight; - int longHeight = imeWindowHeight; - leftFrameStubProxy.setDropShadowHeight(shortHeight, longHeight); - rightFrameStubProxy.setDropShadowHeight(shortHeight, longHeight); + if (symbolInputView.isInflated()) { + CandidateView numberCandidateView = CandidateView.class.cast( + symbolInputView.findViewById(R.id.candidate_view_in_symbol_view)); + numberCandidateView.setInputFrameFoldButtonOnClickListener(createFoldButtonListener( + getNumberKeyboardFrame(), symbolInputView.getNumberKeyboardHeight())); + } // Reset side adjust buttons height. leftFrameStubProxy.resetAdjustButtonBottomMargin(getInputFrameHeight()); @@ -872,85 +850,112 @@ public void setKeyboardHeightRatio(int keyboardHeightRatio) { Resources resources = getResources(); float heightScale = keyboardHeightRatio * 0.01f; - int originalImeWindowHeight = resources.getDimensionPixelSize(R.dimen.ime_window_height); - int originalInputFrameHeight = resources.getDimensionPixelSize(R.dimen.input_frame_height); - imeWindowHeight = Math.round(originalImeWindowHeight * heightScale); + float originalImeWindowHeight = resources.getDimension(R.dimen.ime_window_height); + float originalInputFrameHeight = resources.getDimension(R.dimen.input_frame_height); inputFrameHeight = Math.round(originalInputFrameHeight * heightScale); - // TODO(yoichio): Update SymbolInputView height scale. - // getSymbolInputView().setHeightScale(heightScale); + int minImeWindowHeight = inputFrameHeight + dimensionPixelSize.buttonFrameHeight; + imeWindowHeight = + Math.max(Math.round(originalImeWindowHeight * heightScale), minImeWindowHeight); + symbolInputViewHeight = Math.min(imeWindowHeight, minImeWindowHeight); updateInputFrameHeight(); + getSymbolInputView().setVerticalDimension(symbolInputViewHeight, heightScale); resetHeightDependingComponents(); } - public int getInputFrameHeight() { + @VisibleForTesting + int getInputFrameHeight() { return inputFrameHeight; } - // Getters of child views. - // TODO(hidehiko): Remove (or hide) following methods, in order to split the dependencies to - // those child views from other components. - public CandidateView getCandidateView() { - return CandidateView.class.cast(findViewById(R.id.candidate_view)); + @VisibleForTesting + View getKeyboardFrame() { + return findViewById(R.id.keyboard_frame); } - public ConversionCandidateWordContainerView getConversionCandidateWordContainerView() { - return ConversionCandidateWordContainerView.class.cast( - findViewById(R.id.conversion_candidate_word_container_view)); + View getKeyboardFrameSeparator() { + return findViewById(R.id.keyboard_frame_separator); } - public View getKeyboardFrame() { - return findViewById(R.id.keyboard_frame); + private View getNumberKeyboardFrame() { + return findViewById(R.id.number_keyboard_frame); + } + + @VisibleForTesting + KeyboardView getKeyboardView() { + return KeyboardView.class.cast(findViewById(R.id.keyboard_view)); } - public JapaneseKeyboardView getKeyboardView() { - return JapaneseKeyboardView.class.cast(findViewById(R.id.keyboard_view)); + private CandidateView getKeyboardCandidateView() { + return CandidateView.class.cast(findViewById(R.id.candidate_view)); } - public SymbolInputView getSymbolInputView() { + @VisibleForTesting + SymbolInputView getSymbolInputView() { return SymbolInputView.class.cast(findViewById(R.id.symbol_input_view)); } + @VisibleForTesting View getOverlayView() { return findViewById(R.id.overlay_view); } + @VisibleForTesting LinearLayout getTextInputFrame() { return LinearLayout.class.cast(findViewById(R.id.textinput_frame)); } - FrameLayout getNarrowFrame() { - return FrameLayout.class.cast(findViewById(R.id.narrow_frame)); - } - - ImageView getHardwareCompositionButton() { - return ImageView.class.cast(findViewById(R.id.hardware_composition_button)); - } - - ImageView getWidenButton() { - return ImageView.class.cast(findViewById(R.id.widen_button)); + @VisibleForTesting + NarrowFrameView getNarrowFrame() { + return NarrowFrameView.class.cast(findViewById(R.id.narrow_frame)); } + @VisibleForTesting View getForegroundFrame() { return findViewById(R.id.foreground_frame); } - View getDropShadowTop() { - return findViewById(R.id.dropshadow_top); - } - + @VisibleForTesting View getBottomFrame() { return findViewById(R.id.bottom_frame); } + @VisibleForTesting View getBottomBackground() { return findViewById(R.id.bottom_background); } + @VisibleForTesting + View getButtonFrame() { + return findViewById(R.id.button_frame); + } + + @VisibleForTesting + MozcImageView getMicrophoneButton() { + return MozcImageView.class.cast(findViewById(R.id.microphone_button)); + } + @Override public void trimMemory() { getKeyboardView().trimMemory(); - getCandidateView().trimMemory(); getSymbolInputView().trimMemory(); + candidateViewManager.trimMemory(); + } + + void setGlobeButtonEnabled(boolean globeButtonEnabled) { + getKeyboardView().setGlobeButtonEnabled(globeButtonEnabled); + } + + void setMicrophoneButtonEnabled(boolean microphoneButtonEnabled) { + if (narrowMode) { + buttonFrameVisible = false; + } else { + buttonFrameVisible = microphoneButtonEnabled; + int visibility = buttonFrameVisible ? View.VISIBLE : View.GONE; + getButtonFrame().setVisibility(visibility); + getMicrophoneButton().setVisibility(visibility); + getSymbolInputView().setMicrophoneButtonEnabled(microphoneButtonEnabled); + resetBottomBackgroundHeight(); + } } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java b/src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java new file mode 100644 index 000000000..8434aa48e --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java @@ -0,0 +1,170 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese; + +import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; +import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; +import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; +import org.mozc.android.inputmethod.japanese.view.MozcImageView; +import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable; +import org.mozc.android.inputmethod.japanese.view.Skin; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +/** + * Narrow frame view. + */ +public class NarrowFrameView extends LinearLayout { + + // TODO(hsumita): Move hard coded parameters to dimens.xml or skin. + private static final float BUTTON_CORNOR_RADIUS = 3.5f; // in dip. + private static final float BUTTON_LEFT_OFFSET = 2.0f; + private static final float BUTTON_TOP_OFFSET = 2.0f; + private static final float BUTTON_RIGHT_OFFSET = 2.0f; + private static final float BUTTON_BOTTOM_OFFSET = 2.0f; + + private Skin skin = Skin.getFallbackInstance(); + private CompositionMode hardwareKeyboardCompositionMode = CompositionMode.HIRAGANA; + + public NarrowFrameView(Context context) { + super(context); + } + + public NarrowFrameView(Context context, AttributeSet attrSet) { + super(context, attrSet); + } + + @Override + public void onFinishInflate() { + // Disable h/w acceleration to use a PictureDrawable. + getHardwareCompositionButton().setLayerType(View.LAYER_TYPE_SOFTWARE, null); + getWidenButton().setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + setSkin(skin); + } + + private static Drawable createButtonBackgroundDrawable(float density, Skin skin) { + return BackgroundDrawableFactory.createPressableDrawable( + new RoundRectKeyDrawable( + (int) (BUTTON_LEFT_OFFSET * density), + (int) (BUTTON_TOP_OFFSET * density), + (int) (BUTTON_RIGHT_OFFSET * density), + (int) (BUTTON_BOTTOM_OFFSET * density), + (int) (BUTTON_CORNOR_RADIUS * density), + skin.twelvekeysLayoutPressedFunctionKeyTopColor, + skin.twelvekeysLayoutPressedFunctionKeyBottomColor, + skin.twelvekeysLayoutPressedFunctionKeyHighlightColor, + skin.twelvekeysLayoutPressedFunctionKeyShadowColor), + Optional.of(new RoundRectKeyDrawable( + (int) (BUTTON_LEFT_OFFSET * density), + (int) (BUTTON_TOP_OFFSET * density), + (int) (BUTTON_RIGHT_OFFSET * density), + (int) (BUTTON_BOTTOM_OFFSET * density), + (int) (BUTTON_CORNOR_RADIUS * density), + skin.twelvekeysLayoutReleasedFunctionKeyTopColor, + skin.twelvekeysLayoutReleasedFunctionKeyBottomColor, + skin.twelvekeysLayoutReleasedFunctionKeyHighlightColor, + skin.twelvekeysLayoutReleasedFunctionKeyShadowColor))); + } + + @SuppressWarnings("deprecation") + private void setupImageButton(MozcImageView view, int resourceID) { + float density = getResources().getDisplayMetrics().density; + view.setImageDrawable(skin.getDrawable(getResources(), resourceID)); + view.setBackgroundDrawable(createButtonBackgroundDrawable(density, skin)); + } + + private void updateImageButton() { + setupImageButton(getWidenButton(), R.raw.hardware__function__close); + MozcImageView hardwareCompositionButton = getHardwareCompositionButton(); + if (hardwareKeyboardCompositionMode == CompositionMode.HIRAGANA) { + setupImageButton(hardwareCompositionButton, R.raw.qwerty__function__kana__icon); + hardwareCompositionButton.setContentDescription( + getResources().getString(R.string.cd_key_chartype_to_abc)); + } else { + setupImageButton(hardwareCompositionButton, R.raw.qwerty__function__alphabet__icon); + hardwareCompositionButton.setContentDescription( + getResources().getString(R.string.cd_key_chartype_to_kana)); + } + } + + @SuppressWarnings("deprecation") + public void setSkin(Skin skin) { + this.skin = Preconditions.checkNotNull(skin); + setBackgroundDrawable(skin.narrowFrameBackgroundDrawable); + getNarrowFrameSeparator().setBackgroundDrawable( + skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable()); + updateImageButton(); + } + + public void setEventListener( + final ViewEventListener viewEventListener, OnClickListener widenButtonClickListener) { + Preconditions.checkNotNull(viewEventListener); + Preconditions.checkNotNull(widenButtonClickListener); + + getHardwareCompositionButton().setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + viewEventListener.onFireFeedbackEvent( + FeedbackEvent.NARROW_FRAME_HARDWARE_COMPOSITION_BUTTON_DOWN); + viewEventListener.onHardwareKeyboardCompositionModeChange(CompositionSwitchMode.TOGGLE); + } + }); + getWidenButton().setOnClickListener(widenButtonClickListener); + } + + public void setHardwareCompositionButtonImage(CompositionMode compositionMode) { + this.hardwareKeyboardCompositionMode = Preconditions.checkNotNull(compositionMode); + updateImageButton(); + } + + @VisibleForTesting + MozcImageView getHardwareCompositionButton() { + return MozcImageView.class.cast(findViewById(R.id.hardware_composition_button)); + } + + @VisibleForTesting + MozcImageView getWidenButton() { + return MozcImageView.class.cast(findViewById(R.id.widen_button)); + } + + @VisibleForTesting + View getNarrowFrameSeparator() { + return findViewById(R.id.narrow_frame_separator); + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java b/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java index aeb8f8f79..ed51f3526 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java +++ b/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java @@ -30,11 +30,13 @@ package org.mozc.android.inputmethod.japanese; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.ProbableKeyEvent; import org.mozc.android.inputmethod.japanese.resources.R; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import android.content.Context; @@ -44,8 +46,6 @@ import java.util.List; -import javax.annotation.Nullable; - /** * Converter from primary key code (≒ code point) to Mozc's key event. * @@ -60,10 +60,7 @@ public class PrimaryKeyCodeConverter { public final int keyCodeBackspace; public final int keyCodeEnter; public final int keyCodeChartypeToKana; - public final int keyCodeChartypeTo123; public final int keyCodeChartypeToAbc; - public final int keyCodeChartypeToKana123; - public final int keyCodeChartypeToAbc123; public final int keyCodeSymbol; public final int keyCodeUndo; public final int keyCodeCapslock; @@ -90,10 +87,7 @@ public PrimaryKeyCodeConverter(Context context, ProbableKeyEventGuesser guesser) keyCodeBackspace = res.getInteger(R.integer.key_backspace); keyCodeEnter = res.getInteger(R.integer.key_enter); keyCodeChartypeToKana = res.getInteger(R.integer.key_chartype_to_kana); - keyCodeChartypeTo123 = res.getInteger(R.integer.key_chartype_to_123); keyCodeChartypeToAbc = res.getInteger(R.integer.key_chartype_to_abc); - keyCodeChartypeToKana123 = res.getInteger(R.integer.key_chartype_to_kana_123); - keyCodeChartypeToAbc123 = res.getInteger(R.integer.key_chartype_to_abc_123); keyCodeSymbol = res.getInteger(R.integer.key_symbol); keyCodeUndo = res.getInteger(R.integer.key_undo); keyCodeCapslock = res.getInteger(R.integer.key_capslock); @@ -103,63 +97,63 @@ public PrimaryKeyCodeConverter(Context context, ProbableKeyEventGuesser guesser) this.guesser = Preconditions.checkNotNull(guesser); } - public void setJapaneseKeyboard(@Nullable JapaneseKeyboard japaneseKeyboard) { - guesser.setJapaneseKeyboard(japaneseKeyboard); + public void setKeyboard(Keyboard keyboard) { + guesser.setKeyboard(Preconditions.checkNotNull(keyboard)); } public void setConfiguration(Configuration newConfig) { - guesser.setConfiguration(Preconditions.checkNotNull(newConfig)); + guesser.setConfiguration(Optional.of(newConfig)); } - public ProtoCommands.KeyEvent createMozcKeyEvent( - int primaryCode, List touchEventList) { + public Optional createMozcKeyEvent( + int primaryCode, List touchEventList) { + Preconditions.checkNotNull(touchEventList); + // Space if (primaryCode == ' ') { - return KeycodeConverter.SPECIALKEY_SPACE; + return Optional.of(KeycodeConverter.SPECIALKEY_SPACE); } // Enter if (primaryCode == keyCodeEnter) { - return KeycodeConverter.SPECIALKEY_VIRTUAL_ENTER; + return Optional.of(KeycodeConverter.SPECIALKEY_VIRTUAL_ENTER); } // Backspace if (primaryCode == keyCodeBackspace) { - return KeycodeConverter.SPECIALKEY_BACKSPACE; + return Optional.of(KeycodeConverter.SPECIALKEY_BACKSPACE); } // Up arrow. if (primaryCode == keyCodeUp) { - return KeycodeConverter.SPECIALKEY_UP; + return Optional.of(KeycodeConverter.SPECIALKEY_UP); } // Left arrow. if (primaryCode == keyCodeLeft) { - return KeycodeConverter.SPECIALKEY_VIRTUAL_LEFT; + return Optional.of(KeycodeConverter.SPECIALKEY_VIRTUAL_LEFT); } // Right arrow. if (primaryCode == keyCodeRight) { - return KeycodeConverter.SPECIALKEY_VIRTUAL_RIGHT; + return Optional.of(KeycodeConverter.SPECIALKEY_VIRTUAL_RIGHT); } // Down arrow. if (primaryCode == keyCodeDown) { - return KeycodeConverter.SPECIALKEY_DOWN; + return Optional.of(KeycodeConverter.SPECIALKEY_DOWN); } if (primaryCode > 0) { ProtoCommands.KeyEvent.Builder builder = ProtoCommands.KeyEvent.newBuilder().setKeyCode(primaryCode); - if (guesser != null && touchEventList != null) { + if (!touchEventList.isEmpty()) { List probableKeyEvents = guesser.getProbableKeyEvents(touchEventList); - if (probableKeyEvents != null) { - builder.addAllProbableKeyEvent(probableKeyEvents); - } + builder.addAllProbableKeyEvent(probableKeyEvents); } - return builder.build(); + return Optional.of(builder.build()); } - return null; + return Optional.absent(); } public KeyEventInterface getPrimaryCodeKeyEvent(int primaryCode) { @@ -268,8 +262,8 @@ public int getKeyCode() { } @Override - public KeyEvent getNativeEvent() { - return null; + public Optional getNativeEvent() { + return Optional.absent(); } } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java b/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java index 5dbc0bb9c..5265cdf43 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java @@ -34,21 +34,27 @@ import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory; import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; +import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener; +import org.mozc.android.inputmethod.japanese.keyboard.KeyboardFactory; +import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView; import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage; import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; import org.mozc.android.inputmethod.japanese.model.SymbolMinorCategory; import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; -import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.DescriptionLayoutPolicy; import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.ValueScalingPolicy; import org.mozc.android.inputmethod.japanese.ui.ScrollGuideView; import org.mozc.android.inputmethod.japanese.ui.SpanFactory; import org.mozc.android.inputmethod.japanese.ui.SymbolCandidateLayouter; -import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; +import org.mozc.android.inputmethod.japanese.view.MozcImageButton; +import org.mozc.android.inputmethod.japanese.view.MozcImageView; import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; import org.mozc.android.inputmethod.japanese.view.SymbolMajorCategoryButtonDrawableFactory; import org.mozc.android.inputmethod.japanese.view.TabSelectedBackgroundDrawable; import com.google.common.annotations.VisibleForTesting; @@ -62,36 +68,31 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.LayerDrawable; import android.os.IBinder; +import android.os.Looper; import android.preference.PreferenceManager; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.GestureDetector; -import android.view.GestureDetector.OnGestureListener; -import android.view.Gravity; import android.view.InflateException; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; -import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TabHost; import android.widget.TabHost.OnTabChangeListener; import android.widget.TabHost.TabSpec; import android.widget.TabWidget; import android.widget.TextView; +import java.util.Collections; import java.util.List; /** @@ -113,15 +114,18 @@ public class SymbolInputView extends InOutAnimatedFrameLayout implements MemoryM /** * Adapter for symbol candidate selection. - * Exposed as package private for testing. */ // TODO(hidehiko): make this class static. - class SymbolCandidateSelectListener implements CandidateSelectListener { + @VisibleForTesting class SymbolCandidateSelectListener implements CandidateSelectListener { @Override public void onCandidateSelected(CandidateWord candidateWord, Optional row) { - if (viewEventListener != null) { + Preconditions.checkNotNull(candidateWord); + // When current major category is NUMBER, CandidateView.ConversionCandidateSelectListener + // should handle candidate selection event. + Preconditions.checkState(currentMajorCategory != SymbolMajorCategory.NUMBER); + if (viewEventListener.isPresent()) { // If we are on password field, history shouldn't be updated to protect privacy. - viewEventListener.onSymbolCandidateSelected( + viewEventListener.get().onSymbolCandidateSelected( currentMajorCategory, candidateWord.getValue(), !isPasswordField); } } @@ -130,20 +134,18 @@ public void onCandidateSelected(CandidateWord candidateWord, Optional r /** * Click handler of major category buttons. */ - class MajorCategoryButtonClickListener implements OnClickListener { + @VisibleForTesting class MajorCategoryButtonClickListener implements OnClickListener { private final SymbolMajorCategory majorCategory; MajorCategoryButtonClickListener(SymbolMajorCategory majorCategory) { - if (majorCategory == null) { - throw new NullPointerException("majorCategory should not be null."); - } - this.majorCategory = majorCategory; + this.majorCategory = Preconditions.checkNotNull(majorCategory); } @Override public void onClick(View majorCategorySelectorButton) { - if (viewEventListener != null) { - viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND); + if (viewEventListener.isPresent()) { + viewEventListener.get().onFireFeedbackEvent( + FeedbackEvent.SYMBOL_INPUTVIEW_MAJOR_CATEGORY_SELECTED); } if (emojiEnabled @@ -152,17 +154,17 @@ public void onClick(View majorCategorySelectorButton) { // Ask the user which emoji provider s/he'd like to use. // If the user cancels the dialog, do nothing. maybeInitializeEmojiProviderDialog(getContext()); - if (emojiProviderDialog != null) { + if (emojiProviderDialog.isPresent()) { IBinder token = getWindowToken(); if (token != null) { - MozcUtil.setWindowToken(token, emojiProviderDialog); + MozcUtil.setWindowToken(token, emojiProviderDialog.get()); } else { MozcLog.w("Unknown window token."); } // If a user selects a provider, the dialog handler will set major category // to EMOJI automatically. If s/he cancels, nothing will be happened. - emojiProviderDialog.show(); + emojiProviderDialog.get().show(); return; } } @@ -171,65 +173,77 @@ public void onClick(View majorCategorySelectorButton) { } } - // Manages the relationship between. - static class SymbolTabWidgetViewPagerAdapter extends PagerAdapter + private static class SymbolTabWidgetViewPagerAdapter extends PagerAdapter implements OnTabChangeListener, OnPageChangeListener { private static final int HISTORY_INDEX = 0; private final Context context; private final SymbolCandidateStorage symbolCandidateStorage; - private final ViewEventListener viewEventListener; + private final Optional viewEventListener; private final CandidateSelectListener candidateSelectListener; private final SymbolMajorCategory majorCategory; - private final SkinType skinType; + private Skin skin; private final EmojiProviderType emojiProviderType; private final TabHost tabHost; private final ViewPager viewPager; private final float candidateTextSize; private final float descriptionTextSize; - private View historyViewCache = null; + private Optional historyViewCache = Optional.absent(); private int scrollState = ViewPager.SCROLL_STATE_IDLE; + private boolean feedbackEnabled = true; SymbolTabWidgetViewPagerAdapter( Context context, SymbolCandidateStorage symbolCandidateStorage, - ViewEventListener viewEventListener, CandidateSelectListener candidateSelectListener, - SymbolMajorCategory majorCategory, - SkinType skinType, EmojiProviderType emojiProviderType, - TabHost tabHost, ViewPager viewPager, + Optional viewEventListener, + CandidateSelectListener candidateSelectListener, SymbolMajorCategory majorCategory, + Skin skin, EmojiProviderType emojiProviderType, TabHost tabHost, ViewPager viewPager, float candidateTextSize, float descriptionTextSize) { - Preconditions.checkNotNull(emojiProviderType); + this.context = Preconditions.checkNotNull(context); + this.symbolCandidateStorage = Preconditions.checkNotNull(symbolCandidateStorage); + this.viewEventListener = Preconditions.checkNotNull(viewEventListener); + this.candidateSelectListener = Preconditions.checkNotNull(candidateSelectListener); + this.majorCategory = Preconditions.checkNotNull(majorCategory); + this.skin = Preconditions.checkNotNull(skin); + this.emojiProviderType = Preconditions.checkNotNull(emojiProviderType); + this.tabHost = Preconditions.checkNotNull(tabHost); + this.viewPager = Preconditions.checkNotNull(viewPager); + this.candidateTextSize = Preconditions.checkNotNull(candidateTextSize); + this.descriptionTextSize = Preconditions.checkNotNull(descriptionTextSize); + } + + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + this.skin = skin; + } - this.context = context; - this.symbolCandidateStorage = symbolCandidateStorage; - this.viewEventListener = viewEventListener; - this.candidateSelectListener = candidateSelectListener; - this.majorCategory = majorCategory; - this.skinType = skinType; - this.emojiProviderType = emojiProviderType; - this.tabHost = tabHost; - this.viewPager = viewPager; - this.candidateTextSize = candidateTextSize; - this.descriptionTextSize = descriptionTextSize; + public void setFeedbackEnabled(boolean enabled) { + feedbackEnabled = enabled; } private void maybeResetHistoryView() { - if (viewPager.getCurrentItem() != HISTORY_INDEX && historyViewCache != null) { + if (viewPager.getCurrentItem() != HISTORY_INDEX && historyViewCache.isPresent()) { resetHistoryView(); } } private void resetHistoryView() { + if (!historyViewCache.isPresent()) { + return; + } CandidateList candidateList = symbolCandidateStorage.getCandidateList(majorCategory.minorCategories.get(0)); + View noHistoryView = historyViewCache.get().findViewById(R.id.symbol_input_no_history); if (candidateList.getCandidatesCount() == 0) { - historyViewCache.findViewById(R.id.symbol_input_no_history).setVisibility(View.VISIBLE); + noHistoryView.setVisibility(View.VISIBLE); + TextView.class.cast(historyViewCache.get().findViewById(R.id.symbol_input_no_history_text)) + .setTextColor(skin.candidateValueTextColor); } else { - historyViewCache.findViewById(R.id.symbol_input_no_history).setVisibility(View.GONE); + noHistoryView.setVisibility(View.GONE); } - SymbolCandidateView.class.cast( - historyViewCache.findViewById(R.id.symbol_input_candidate_view)).update(candidateList); + SymbolCandidateView.class.cast(historyViewCache.get().findViewById( + R.id.symbol_input_candidate_view)).update(candidateList); } @Override @@ -250,10 +264,6 @@ public void onPageSelected(int position) { tabHost.setOnTabChangedListener(null); tabHost.setCurrentTab(position); tabHost.setOnTabChangedListener(this); - - if (viewEventListener != null) { - viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND); - } } @Override @@ -265,8 +275,9 @@ public void onTabChanged(String tabId) { } viewPager.setCurrentItem(position, false); - if (viewEventListener != null) { - viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND); + if (feedbackEnabled && viewEventListener.isPresent()) { + viewEventListener.get().onFireFeedbackEvent( + FeedbackEvent.SYMBOL_INPUTVIEW_MINOR_CATEGORY_SELECTED); } } @@ -293,13 +304,21 @@ public Object instantiateItem(ViewGroup container, int position) { symbolCandidateView.setCandidateSelectListener(candidateSelectListener); symbolCandidateView.setMinColumnWidth( context.getResources().getDimension(majorCategory.minColumnWidthResourceId)); - symbolCandidateView.setSkinType(skinType); + symbolCandidateView.setSkin(skin); symbolCandidateView.setEmojiProviderType(emojiProviderType); - symbolCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize); + if (majorCategory.layoutPolicy == DescriptionLayoutPolicy.GONE) { + // As it's guaranteed for descriptions not to be shown, + // show values using additional space where is reserved for descriptions. + // This makes Emoji bigger. + symbolCandidateView.setCandidateTextDimension(candidateTextSize + descriptionTextSize, 0); + } else { + symbolCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize); + } + symbolCandidateView.setDescriptionLayoutPolicy(majorCategory.layoutPolicy); // Set candidate contents. if (position == HISTORY_INDEX) { - historyViewCache = view; + historyViewCache = Optional.of(view); resetHistoryView(); } else { symbolCandidateView.update(symbolCandidateStorage.getCandidateList( @@ -309,7 +328,7 @@ public Object instantiateItem(ViewGroup container, int position) { ScrollGuideView scrollGuideView = ScrollGuideView.class.cast(view.findViewById(R.id.symbol_input_scroll_guide_view)); - scrollGuideView.setSkinType(skinType); + scrollGuideView.setSkin(skin); // Connect guide and candidate view. scrollGuideView.setScroller(symbolCandidateView.scroller); @@ -322,73 +341,12 @@ public Object instantiateItem(ViewGroup container, int position) { @Override public void destroyItem(ViewGroup collection, int position, Object view) { if (position == HISTORY_INDEX) { - historyViewCache = null; + historyViewCache = Optional.absent(); } collection.removeView(View.class.cast(view)); } } - /** - * The text view for the minor category tab. - * The most thing is as same as base TextView, but if the text is too long to fit in - * the view, this view automatically scales the text horizontally. (If there is enough - * space, doesn't widen.) - */ - private static class TabTextView extends TextView { - - /** Cached Paint instance to measure the text. */ - private final Paint paint = new Paint(); - - TabTextView(Context context) { - super(context); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - resetTextXSize(); - } - - private void resetTextXSize() { - // Reset the paint instance. - Paint paint = this.paint; - paint.reset(); - paint.setAntiAlias(true); - paint.setTextSize(getTextSize()); - paint.setTypeface(getTypeface()); - - // Measure the width for each line. - CharSequence text = getText().toString(); - float maxTextWidth = 0; - int beginIndex = 0; - for (int i = beginIndex; i < text.length(); ++i) { - if (text.charAt(i) == '\n') { - // Split the line. - float textWidth = paint.measureText(text, beginIndex, i); - if (textWidth > maxTextWidth) { - maxTextWidth = textWidth; - } - // Exclude '\n'. - beginIndex = i + 1; - } - } - { - // Last line. - float textWidth = paint.measureText(text, beginIndex, text.length()); - if (textWidth > maxTextWidth) { - maxTextWidth = textWidth; - } - } - - // Calculate scale factory. Note that 0.98f is the heuristic value, - // in order to avoid wrapping lines by sub px calculations just in case. - float scaleX = (getWidth() - getPaddingLeft() - getPaddingRight()) * 0.98f / maxTextWidth; - - // Cap the scaleX by 1f not to widen. - setTextScaleX(Math.min(scaleX, 1f)); - } - } - /** * An event listener for the menu dialog window. */ @@ -396,7 +354,7 @@ private class EmojiProviderDialogListener implements DialogInterface.OnClickList private final Context context; EmojiProviderDialogListener(Context context) { - this.context = context; + this.context = Preconditions.checkNotNull(context); } @Override @@ -422,11 +380,10 @@ public void onClick(DialogInterface dialog, int which) { * The differences from CandidateView.CandidateWordViewForConversion are * 1) this class scrolls horizontally 2) the layout algorithm is simpler. */ - static class SymbolCandidateView extends CandidateWordView { + private static class SymbolCandidateView extends CandidateWordView { private static final String DESCRIPTION_DELIMITER = "\n"; - private View scrollGuideView = null; - private GestureDetector gestureDetector = null; + private Optional scrollGuideView = Optional.absent(); public SymbolCandidateView(Context context) { super(context, Orientation.VERTICAL); @@ -442,7 +399,7 @@ public SymbolCandidateView(Context context, AttributeSet attributeSet, int defau // Shared instance initializer. { - setBackgroundDrawableType(DrawableType.SYMBOL_CANDIDATE_BACKGROUND); + setSpanBackgroundDrawableType(DrawableType.SYMBOL_CANDIDATE_BACKGROUND); Resources resources = getResources(); scroller.setDecayRate( resources.getInteger(R.integer.symbol_input_scroller_velocity_decay_rate) / 1000000f); @@ -452,25 +409,27 @@ public SymbolCandidateView(Context context, AttributeSet attributeSet, int defau } void setCandidateTextDimension(float textSize, float descriptionTextSize) { - Preconditions.checkArgument(textSize > 0); - Preconditions.checkArgument(descriptionTextSize > 0); + Preconditions.checkArgument(textSize >= 0); + Preconditions.checkArgument(descriptionTextSize >= 0); Resources resources = getResources(); float valueHorizontalPadding = - resources.getDimension(R.dimen.candidate_horizontal_padding_size); + resources.getDimension(R.dimen.symbol_candidate_horizontal_padding_size); float descriptionHorizontalPadding = resources.getDimension(R.dimen.symbol_description_right_padding); float descriptionVerticalPadding = resources.getDimension(R.dimen.symbol_description_bottom_padding); + float separatorWidth = resources.getDimensionPixelSize(R.dimen.candidate_separator_width); + carrierEmojiRenderHelper.setCandidateTextSize(textSize); candidateLayoutRenderer.setValueTextSize(textSize); candidateLayoutRenderer.setValueHorizontalPadding(valueHorizontalPadding); candidateLayoutRenderer.setValueScalingPolicy(ValueScalingPolicy.UNIFORM); candidateLayoutRenderer.setDescriptionTextSize(descriptionTextSize); candidateLayoutRenderer.setDescriptionHorizontalPadding(descriptionHorizontalPadding); candidateLayoutRenderer.setDescriptionVerticalPadding(descriptionVerticalPadding); - candidateLayoutRenderer.setDescriptionLayoutPolicy(DescriptionLayoutPolicy.OVERLAY); + candidateLayoutRenderer.setSeparatorWidth(separatorWidth); SpanFactory spanFactory = new SpanFactory(); spanFactory.setValueTextSize(textSize); @@ -492,32 +451,31 @@ void setMinColumnWidth(float minColumnWidth) { updateLayouter(); } - void setOnGestureListener(OnGestureListener gestureListener) { - if (gestureListener == null) { - gestureDetector = null; - } else { - gestureDetector = new GestureDetector(getContext(), gestureListener); + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (scrollGuideView.isPresent()) { + scrollGuideView.get().invalidate(); } } + void setScrollIndicator(View scrollGuideView) { + this.scrollGuideView = Optional.of(scrollGuideView); + } + @Override - public boolean onTouchEvent(MotionEvent event) { - if (gestureDetector != null && gestureDetector.onTouchEvent(event)) { - return true; - } - return super.onTouchEvent(event); + protected Drawable getViewBackgroundDrawable(Skin skin) { + return Preconditions.checkNotNull(skin).symbolCandidateViewBackgroundDrawable; } @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (scrollGuideView != null) { - scrollGuideView.invalidate(); - } + public void setSkin(Skin skin) { + super.setSkin(skin); + candidateLayoutRenderer.setSeparatorColor(skin.symbolCandidateBackgroundSeparatorColor); } - void setScrollIndicator(View scrollGuideView) { - this.scrollGuideView = scrollGuideView; + public void setDescriptionLayoutPolicy(DescriptionLayoutPolicy policy) { + candidateLayoutRenderer.setDescriptionLayoutPolicy(Preconditions.checkNotNull(policy)); } } @@ -539,59 +497,46 @@ public void onAnimationEnd(Animation animation) { */ static final KeyboardSpecificationName SPEC_NAME = new KeyboardSpecificationName("SYMBOL_INPUT_VIEW", 0, 1, 0); - // Source ID of the delete button for logging usage stats. + // Source ID of the delete/enter button for logging usage stats. private static final int DELETE_BUTTON_SOURCE_ID = 1; - - // TODO(hidehiko): move these parameters to skin instance. - private static final float BUTTON_CORNOR_RADIUS = 3.5f; // in dip. - private static final float BUTTON_LEFT_OFFSET = 2.0f; - private static final float BUTTON_TOP_OFFSET = 2.0f; - private static final float BUTTON_RIGHT_OFFSET = 2.0f; - private static final float BUTTON_BOTTOM_OFFSET = 2.0f; - - private static final int MAJOR_CATEGORY_TOP_COLOR = 0xFFF5F5F5; - private static final int MAJOR_CATEGORY_BOTTOM_COLOR = 0xFFD2D2D2; - private static final int MAJOR_CATEGORY_PRESSED_TOP_COLOR = 0xFFAAAAAA; - private static final int MAJOR_CATEGORY_PRESSED_BOTTOM_COLOR = 0xFF828282; - private static final int MAJOR_CATEGORY_SHADOW_COLOR = 0x57000000; - - // TODO(hidehiko): This parameter is not fixed yet. Needs to revisit again. - private static final float SYMBOL_VIEW_MINOR_CATEGORY_TAB_SELECTED_HEIGHT = 6f; + private static final int ENTER_BUTTON_SOURCE_ID = 2; private static final int NUM_TABS = 6; - private SymbolCandidateStorage symbolCandidateStorage; + private Optional viewHeight = Optional.absent(); + private Optional numberKeyboardHeight = Optional.absent(); + private Optional keyboardHeightScale = Optional.absent(); - @VisibleForTesting SymbolMajorCategory currentMajorCategory; + private Optional symbolCandidateStorage = Optional.absent(); + + @VisibleForTesting SymbolMajorCategory currentMajorCategory = SymbolMajorCategory.NUMBER; @VisibleForTesting boolean emojiEnabled; private boolean isPasswordField; @VisibleForTesting EmojiProviderType emojiProviderType = EmojiProviderType.NONE; @VisibleForTesting SharedPreferences sharedPreferences; - @VisibleForTesting AlertDialog emojiProviderDialog; + @VisibleForTesting Optional emojiProviderDialog = Optional.absent(); - private ViewEventListener viewEventListener; + private Optional viewEventListener = Optional.absent(); private final KeyEventButtonTouchListener deleteKeyEventButtonTouchListener = createDeleteKeyEventButtonTouchListener(getResources()); - private OnClickListener closeButtonClickListener = null; + private final KeyEventButtonTouchListener enterKeyEventButtonTouchListener = + createEnterKeyEventButtonTouchListener(getResources()); + private Optional closeButtonClickListener = Optional.absent(); + private Optional microphoneButtonClickListener = Optional.absent(); private final SymbolCandidateSelectListener symbolCandidateSelectListener = new SymbolCandidateSelectListener(); - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; - private final MozcDrawableFactory mozcDrawableFactory = new MozcDrawableFactory(getResources()); + private Skin skin = Skin.getFallbackInstance(); private final SymbolMajorCategoryButtonDrawableFactory majorCategoryButtonDrawableFactory = - new SymbolMajorCategoryButtonDrawableFactory( - mozcDrawableFactory, - MAJOR_CATEGORY_TOP_COLOR, - MAJOR_CATEGORY_BOTTOM_COLOR, - MAJOR_CATEGORY_PRESSED_TOP_COLOR, - MAJOR_CATEGORY_PRESSED_BOTTOM_COLOR, - MAJOR_CATEGORY_SHADOW_COLOR, - BUTTON_CORNOR_RADIUS * getResources().getDisplayMetrics().density); + new SymbolMajorCategoryButtonDrawableFactory(getResources()); // Candidate text size in dip. private float candidateTextSize; // Description text size in dip. private float desciptionTextSize; + private Optional keyEventHandler = Optional.absent(); + private boolean isMicrophoneButtonEnabled; + private boolean popupEnabled; public SymbolInputView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); @@ -607,27 +552,28 @@ public SymbolInputView(Context context, AttributeSet attrs) { { setOutAnimationListener(new OutAnimationAdapter()); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + sharedPreferences = + Preconditions.checkNotNull(PreferenceManager.getDefaultSharedPreferences(getContext())); } private static KeyEventButtonTouchListener createDeleteKeyEventButtonTouchListener( Resources resources) { - // Use 8 as default value of backspace key code (only for testing). - // This code is introduced just for testing purpose due to AndroidMock's limitation. - // When we move to EasyMock, we can remove this method. - int ucharBackspace = resources == null ? 8 : resources.getInteger(R.integer.uchar_backspace); - return new KeyEventButtonTouchListener(DELETE_BUTTON_SOURCE_ID, ucharBackspace); + return new KeyEventButtonTouchListener( + DELETE_BUTTON_SOURCE_ID, resources.getInteger(R.integer.uchar_backspace)); } + private static KeyEventButtonTouchListener createEnterKeyEventButtonTouchListener( + Resources resources) { + return new KeyEventButtonTouchListener( + ENTER_BUTTON_SOURCE_ID, resources.getInteger(R.integer.uchar_linefeed)); + } boolean isInflated() { return getChildCount() > 0; } void inflateSelf() { - if (isInflated()) { - throw new IllegalStateException("The symbol input view is already inflated."); - } + Preconditions.checkState(!isInflated(), "The symbol input view is already inflated."); // Hack: Because we wrap the real context to inject "retrying" for Drawable loading, // LayoutInflater.from(getContext()).getContext() may be different from getContext(). @@ -649,17 +595,112 @@ void inflateSelf() { * So, instead, we define another onFinishInflate method and invoke this manually. */ protected void onFinishInflateSelf() { - initializeMajorCategoryButtons(); + if (viewHeight.isPresent() && keyboardHeightScale.isPresent()) { + setVerticalDimension(viewHeight.get(), keyboardHeightScale.get()); + } + initializeMinorCategoryTab(); initializeCloseButton(); initializeDeleteButton(); + initializeEnterButton(); + initializeMicrophoneButton(); + + // Set TouchListener that does nothing. Without this hack, state_pressed event + // will be propagated to close / enter key and the drawable will be changed to + // state_pressed one unexpectedly. Note that those keys are NOT children of this view. + // Setting ClickListener to the key seems to suppress this unexpected highlight, too, + // but we want to keep the current TouchListener for the enter key. + OnTouchListener doNothingOnTouchListener = new OnTouchListener() { + @Override + public boolean onTouch(View button, android.view.MotionEvent event) { + return true; + } + }; + for (int id : new int[] {R.id.button_frame_in_symbol_view, + R.id.symbol_view_backspace_separator, + R.id.symbol_major_category, + R.id.symbol_separator_1, + R.id.symbol_separator_2, + R.id.symbol_separator_3, + R.id.symbol_view_close_button_separator, + R.id.symbol_view_enter_button_separator}) { + findViewById(id).setOnTouchListener(doNothingOnTouchListener); + } + + KeyboardView keyboardView = KeyboardView.class.cast(findViewById(R.id.number_keyboard)); + keyboardView.setPopupEnabled(popupEnabled); + keyboardView.setKeyEventHandler(new KeyEventHandler( + Looper.getMainLooper(), + new KeyboardActionListener() { + @Override + public void onRelease(int keycode) { + } + + @Override + public void onPress(int keycode) { + if (viewEventListener.isPresent()) { + viewEventListener.get().onFireFeedbackEvent(FeedbackEvent.KEY_DOWN); + } + } + + @Override + public void onKey(int primaryCode, List touchEventList) { + if (keyEventHandler.isPresent()) { + keyEventHandler.get().sendKey(primaryCode, touchEventList); + } + } + + @Override + public void onCancel() { + } + }, + getResources().getInteger(R.integer.config_repeat_key_delay), + getResources().getInteger(R.integer.config_repeat_key_interval), + getResources().getInteger(R.integer.config_long_press_key_delay))); - resetMajorCategoryBackground(); - resetTabBackground(); enableEmoji(emojiEnabled); + + // Disable h/w acceleration to use a PictureDrawable. + for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) { + getMajorCategoryButton(majorCategory).setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + updateSkinAwareDrawable(); reset(); } + private static void setLayoutHeight(View view, int height) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.height = height; + view.setLayoutParams(layoutParams); + } + + public void setVerticalDimension(int symbolInputViewHeight, float keyboardHeightScale) { + this.viewHeight = Optional.of(symbolInputViewHeight); + this.keyboardHeightScale = Optional.of(keyboardHeightScale); + + Resources resources = getResources(); + float originalMajorCategoryHeight = + resources.getDimension(R.dimen.symbol_view_major_category_height); + int majorCategoryHeight = Math.round(originalMajorCategoryHeight * keyboardHeightScale); + this.numberKeyboardHeight = Optional.of( + symbolInputViewHeight - majorCategoryHeight + - resources.getDimensionPixelSize(R.dimen.button_frame_height)); + + if (!isInflated()) { + return; + } + + setLayoutHeight(this, symbolInputViewHeight); + setLayoutHeight(getMajorCategoryFrame(), majorCategoryHeight); + setLayoutHeight(findViewById(R.id.number_keyboard), numberKeyboardHeight.get()); + setLayoutHeight(findViewById(R.id.number_keyboard_frame), LayoutParams.WRAP_CONTENT); + } + + public int getNumberKeyboardHeight() { + return numberKeyboardHeight.get(); + } + private void resetCandidateViewPager() { if (!isInflated()) { return; @@ -667,52 +708,59 @@ private void resetCandidateViewPager() { ViewPager candidateViewPager = getCandidateViewPager(); TabHost tabHost = getTabHost(); + Preconditions.checkState(symbolCandidateStorage.isPresent()); SymbolTabWidgetViewPagerAdapter adapter = new SymbolTabWidgetViewPagerAdapter( getContext(), - symbolCandidateStorage, viewEventListener, symbolCandidateSelectListener, - currentMajorCategory, skinType, emojiProviderType, tabHost, candidateViewPager, + symbolCandidateStorage.get(), viewEventListener, symbolCandidateSelectListener, + currentMajorCategory, skin, emojiProviderType, tabHost, candidateViewPager, candidateTextSize, desciptionTextSize); candidateViewPager.setAdapter(adapter); candidateViewPager.setOnPageChangeListener(adapter); tabHost.setOnTabChangedListener(adapter); } - private void resetMajorCategoryBackground() { - View view = findViewById(R.id.symbol_major_category); + @SuppressWarnings("deprecation") + private void updateMajorCategoryBackgroundSkin() { + View view = getMajorCategoryFrame(); if (view != null) { - if (skinType == null) { - view.setBackgroundColor(Color.BLACK); - } else { - view.setBackgroundResource(skinType.windowBackgroundResourceId); - } + view.setBackgroundDrawable( + skin.symbolMajorCategoryBackgroundDrawable.getConstantState().newDrawable()); } } - private void setMozcDrawable(ImageView imageView, int resourceId) { - Optional drawable = mozcDrawableFactory.getDrawable(resourceId); - if (drawable.isPresent()) { - imageView.setImageDrawable(drawable.get()); + @SuppressWarnings("deprecation") + private void updateMinorCategoryBackgroundSkin() { + View view = getMinorCategoryFrame(); + if (view != null) { + view.setBackgroundDrawable( + skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable()); } } + @SuppressWarnings("deprecation") + private void updateNumberKeyboardSkin() { + getNumberKeyboardView().setSkin(skin); + findViewById(R.id.number_frame).setBackgroundDrawable( + skin.windowBackgroundDrawable.getConstantState().newDrawable()); + findViewById(R.id.button_frame_in_symbol_view).setBackgroundDrawable( + skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable()); + } + /** * Sets click event handlers to each major category button. * It is necessary that the inflation has been done before this method invocation. */ @SuppressWarnings("deprecation") - private void initializeMajorCategoryButtons() { + private void updateMajorCategoryButtonsSkin() { + Resources resources = getResources(); for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) { - ImageView view = ImageView.class.cast(findViewById(majorCategory.buttonResourceId)); - if (view == null) { - throw new IllegalStateException( - "The view corresponding to " + majorCategory.name() + " is not found."); - } + MozcImageButton view = getMajorCategoryButton(majorCategory); + Preconditions.checkState( + view != null, "The view corresponding to " + majorCategory.name() + " is not found."); view.setOnClickListener(new MajorCategoryButtonClickListener(majorCategory)); - setMozcDrawable(view, majorCategory.buttonImageResourceId); - switch (majorCategory) { - case SYMBOL: + case NUMBER: view.setBackgroundDrawable( majorCategoryButtonDrawableFactory.createLeftButtonDrawable()); break; @@ -725,16 +773,16 @@ private void initializeMajorCategoryButtons() { majorCategoryButtonDrawableFactory.createCenterButtonDrawable()); break; } + view.setImageDrawable(skin.getDrawable(resources, majorCategory.buttonImageResourceId)); + // Update the padding since setBackgroundDrawable() overwrites it. + view.setMaxImageHeight( + resources.getDimensionPixelSize(majorCategory.maxImageHeightResourceId)); } } private void initializeMinorCategoryTab() { - TabHost tabhost = TabHost.class.cast(findViewById(android.R.id.tabhost)); + TabHost tabhost = getTabHost(); tabhost.setup(); - - float textSize = getResources().getDimension(R.dimen.symbol_view_minor_category_text_size); - int textColor = getResources().getColor(android.R.color.black); - // Create NUM_TABS (= 6) tabs. // Note that we may want to change the number of tabs, however due to the limitation of // the current TabHost implementation, it is difficult. Fortunately, all major categories @@ -742,12 +790,9 @@ private void initializeMinorCategoryTab() { for (int i = 0; i < NUM_TABS; ++i) { // The tab's id is the index of the tab. TabSpec tab = tabhost.newTabSpec(String.valueOf(i)); - TextView textView = new TabTextView(getContext()); - textView.setTypeface(Typeface.DEFAULT_BOLD); - textView.setTextColor(textColor); - textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); - textView.setGravity(Gravity.CENTER); - tab.setIndicator(textView); + MozcImageView view = new MozcImageView(getContext()); + view.setSoundEffectsEnabled(false); + tab.setIndicator(view); // Set dummy view for the content. The actual content will be managed by ViewPager. tab.setContent(R.id.symbol_input_dummy); tabhost.addTab(tab); @@ -759,79 +804,84 @@ private void initializeMinorCategoryTab() { } @SuppressWarnings("deprecation") - private void resetTabBackground() { + private void updateTabBackgroundSkin() { if (!isInflated()) { return; } - - float density = getResources().getDisplayMetrics().density; - - TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs)); + getTabHost().setBackgroundDrawable( + skin.windowBackgroundDrawable.getConstantState().newDrawable()); + TabWidget tabWidget = getTabWidget(); for (int i = 0; i < tabWidget.getTabCount(); ++i) { View view = tabWidget.getChildTabViewAt(i); - view.setBackgroundDrawable(createTabBackgroundDrawable(skinType, density)); + view.setBackgroundDrawable(createTabBackgroundDrawable(skin)); } } - private static Drawable createTabBackgroundDrawable(SkinType skinType, float density) { + private static Drawable createTabBackgroundDrawable(Skin skin) { + Preconditions.checkNotNull(skin); return new LayerDrawable(new Drawable[] { BackgroundDrawableFactory.createSelectableDrawable( new TabSelectedBackgroundDrawable( - (int) (SYMBOL_VIEW_MINOR_CATEGORY_TAB_SELECTED_HEIGHT * density), - skinType.symbolMinorCategoryTabSelectedColor), - null), - BackgroundDrawableFactory.createPressableDrawable( - new ColorDrawable(skinType.symbolMinorCategoryTabPressedColor), null), + Math.round(skin.symbolMinorIndicatorHeightDimension), + skin.symbolMinorCategoryTabSelectedColor), + Optional.absent()), + createMinorButtonBackgroundDrawable(skin) }); } - private void resetTabText() { + private void resetTabImageForMinorCategory() { if (!isInflated()) { return; } - - TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs)); + TabWidget tabWidget = getTabWidget(); List minorCategoryList = currentMajorCategory.minorCategories; - for (int i = 0; i < tabWidget.getChildCount(); ++i) { - TextView textView = TextView.class.cast(tabWidget.getChildTabViewAt(i)); - textView.setText(minorCategoryList.get(i).textResourceId); + int definedTabSize = Math.min(minorCategoryList.size(), tabWidget.getChildCount()); + for (int i = 0; i < definedTabSize; ++i) { + MozcImageView view = MozcImageView.class.cast(tabWidget.getChildTabViewAt(i)); + SymbolMinorCategory symbolMinorCategory = minorCategoryList.get(i); + view.setRawId(symbolMinorCategory.drawableResourceId); + if (symbolMinorCategory.maxImageHeightResourceId != SymbolMinorCategory.INVALID_RESOURCE_ID) { + view.setMaxImageHeight( + getResources().getDimensionPixelSize(symbolMinorCategory.maxImageHeightResourceId)); + } + if (symbolMinorCategory.contentDescriptionResourceId + != SymbolMinorCategory.INVALID_RESOURCE_ID) { + view.setContentDescription( + getResources().getString(symbolMinorCategory.contentDescriptionResourceId)); + } } } - private static Drawable createButtonBackgroundDrawable(SkinType skinType, float density) { + private static Drawable createMajorButtonBackgroundDrawable(Skin skin) { + int padding = Math.round(skin.symbolMajorButtonPaddingDimension); + int round = Math.round(skin.symbolMajorButtonRoundDimension); return BackgroundDrawableFactory.createPressableDrawable( new RoundRectKeyDrawable( - (int) (BUTTON_LEFT_OFFSET * density), - (int) (BUTTON_TOP_OFFSET * density), - (int) (BUTTON_RIGHT_OFFSET * density), - (int) (BUTTON_BOTTOM_OFFSET * density), - (int) (BUTTON_CORNOR_RADIUS * density), - skinType.symbolPressedFunctionKeyTopColor, - skinType.symbolPressedFunctionKeyBottomColor, - skinType.symbolPressedFunctionKeyHighlightColor, - skinType.symbolPressedFunctionKeyShadowColor), - new RoundRectKeyDrawable( - (int) (BUTTON_LEFT_OFFSET * density), - (int) (BUTTON_TOP_OFFSET * density), - (int) (BUTTON_RIGHT_OFFSET * density), - (int) (BUTTON_BOTTOM_OFFSET * density), - (int) (BUTTON_CORNOR_RADIUS * density), - skinType.symbolReleasedFunctionKeyTopColor, - skinType.symbolReleasedFunctionKeyBottomColor, - skinType.symbolReleasedFunctionKeyHighlightColor, - skinType.symbolReleasedFunctionKeyShadowColor)); + padding, padding, padding, padding, round, + skin.symbolPressedFunctionKeyTopColor, + skin.symbolPressedFunctionKeyBottomColor, + skin.symbolPressedFunctionKeyHighlightColor, + skin.symbolPressedFunctionKeyShadowColor), + Optional.of(new RoundRectKeyDrawable( + padding, padding, padding, padding, round, + skin.symbolReleasedFunctionKeyTopColor, + skin.symbolReleasedFunctionKeyBottomColor, + skin.symbolReleasedFunctionKeyHighlightColor, + skin.symbolReleasedFunctionKeyShadowColor))); + } + + private static Drawable createMinorButtonBackgroundDrawable(Skin skin) { + return BackgroundDrawableFactory.createPressableDrawable( + new ColorDrawable(skin.symbolMinorCategoryTabPressedColor), + Optional.absent()); } @SuppressWarnings("deprecation") private void initializeCloseButton() { ImageView closeButton = ImageView.class.cast(findViewById(R.id.symbol_view_close_button)); - if (closeButtonClickListener != null) { - closeButton.setOnClickListener(closeButtonClickListener); + if (closeButtonClickListener.isPresent()) { + closeButton.setOnClickListener(closeButtonClickListener.get()); } - closeButton.setBackgroundDrawable( - createButtonBackgroundDrawable(skinType, getResources().getDisplayMetrics().density)); - setMozcDrawable(closeButton, R.raw.symbol__function__close); - closeButton.setPadding(2, 2, 2, 2); } /** @@ -840,37 +890,77 @@ private void initializeCloseButton() { */ @SuppressWarnings("deprecation") private void initializeDeleteButton() { - ImageView deleteButton = ImageView.class.cast(findViewById(R.id.symbol_view_delete_button)); + MozcImageView deleteButton = + MozcImageView.class.cast(findViewById(R.id.symbol_view_delete_button)); deleteButton.setOnTouchListener(deleteKeyEventButtonTouchListener); - deleteButton.setBackgroundDrawable( - createButtonBackgroundDrawable(skinType, getResources().getDisplayMetrics().density)); - setMozcDrawable(deleteButton, R.raw.symbol__function__delete); - deleteButton.setPadding(0, 0, 0, 0); } - TabHost getTabHost() { + /** c.f., {@code initializeDeleteButton}. */ + @SuppressWarnings("deprecation") + private void initializeEnterButton() { + ImageView enterButton = ImageView.class.cast(findViewById(R.id.symbol_view_enter_button)); + enterButton.setOnTouchListener(enterKeyEventButtonTouchListener); + } + + private void initializeMicrophoneButton() { + MozcImageView microphoneButton = getMicrophoneButton(); + if (microphoneButtonClickListener.isPresent()) { + microphoneButton.setOnClickListener(microphoneButtonClickListener.get()); + } + microphoneButton.setVisibility(isMicrophoneButtonEnabled ? VISIBLE : GONE); + } + + @SuppressWarnings("deprecation") + private void updateSeparatorsSkin() { + Resources resources = getResources(); + int minorPaddingSize = (int) resources.getFraction( + R.fraction.symbol_separator_padding_fraction, + resources.getDimensionPixelSize(R.dimen.button_frame_height), 0); + findViewById(R.id.symbol_view_backspace_separator).setBackgroundDrawable( + new InsetDrawable(new ColorDrawable(skin.symbolSeparatorColor), + 0, minorPaddingSize, 0, minorPaddingSize)); + int majorPaddingSize = (int) resources.getFraction( + R.fraction.symbol_separator_padding_fraction, + resources.getDimensionPixelSize(R.dimen.symbol_view_major_category_height), 0); + InsetDrawable separator = new InsetDrawable( + new ColorDrawable(skin.symbolSeparatorColor), 0, majorPaddingSize, 0, majorPaddingSize); + for (int id : new int[] {R.id.symbol_view_close_button_separator, + R.id.symbol_view_enter_button_separator}) { + findViewById(id).setBackgroundDrawable(separator.getConstantState().newDrawable()); + } + + for (int id : new int[] {R.id.symbol_separator_1, + R.id.symbol_separator_3}) { + findViewById(id).setBackgroundDrawable( + skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable()); + } + findViewById(R.id.symbol_separator_2).setBackgroundDrawable( + skin.symbolSeparatorAboveMajorCategoryBackgroundDrawable + .getConstantState().newDrawable()); + } + + @VisibleForTesting TabHost getTabHost() { return TabHost.class.cast(findViewById(android.R.id.tabhost)); } - ViewPager getCandidateViewPager() { + private ViewPager getCandidateViewPager() { return ViewPager.class.cast(findViewById(R.id.symbol_input_candidate_view_pager)); } - ImageButton getMajorCategoryButton(SymbolMajorCategory majorCategory) { - if (majorCategory == null) { - throw new NullPointerException("majorCategory shouldn't be null."); - } - return ImageButton.class.cast(findViewById(majorCategory.buttonResourceId)); + @VisibleForTesting MozcImageButton getMajorCategoryButton(SymbolMajorCategory majorCategory) { + Preconditions.checkNotNull(majorCategory); + return MozcImageButton.class.cast(findViewById(majorCategory.buttonResourceId)); } - View getEmojiDisabledMessageView() { + @VisibleForTesting View getEmojiDisabledMessageView() { return findViewById(R.id.symbol_emoji_disabled_message_view); } public void setEmojiEnabled(boolean unicodeEmojiEnabled, boolean carrierEmojiEnabled) { this.emojiEnabled = unicodeEmojiEnabled || carrierEmojiEnabled; enableEmoji(this.emojiEnabled); - symbolCandidateStorage.setEmojiEnabled(unicodeEmojiEnabled, carrierEmojiEnabled); + Preconditions.checkState(symbolCandidateStorage.isPresent()); + symbolCandidateStorage.get().setEmojiEnabled(unicodeEmojiEnabled, carrierEmojiEnabled); } public void setPasswordField(boolean isPasswordField) { @@ -883,36 +973,47 @@ private void enableEmoji(boolean enableEmoji) { return; } - ImageButton imageButton = getMajorCategoryButton(SymbolMajorCategory.EMOJI); + MozcImageButton imageButton = getMajorCategoryButton(SymbolMajorCategory.EMOJI); imageButton.setBackgroundDrawable( majorCategoryButtonDrawableFactory.createRightButtonDrawable(enableEmoji)); + // Update the padding since setBackgroundDrawable() overwrites it. + imageButton.setMaxImageHeight(getResources().getDimensionPixelSize( + SymbolMajorCategory.EMOJI.maxImageHeightResourceId)); } - /** - * Resets the status. - */ - void reset() { - // the current minor category is also updated in setMajorCategory. - setMajorCategory(SymbolMajorCategory.SYMBOL); + void resetToMajorCategory(Optional category) { + Preconditions.checkNotNull(category); + setMajorCategory(category.or(currentMajorCategory)); deleteKeyEventButtonTouchListener.reset(); + enterKeyEventButtonTouchListener.reset(); + } + + @VisibleForTesting void reset() { + // the current minor category is also updated in setMajorCategory. + resetToMajorCategory(Optional.of(SymbolMajorCategory.NUMBER)); } @Override public void setVisibility(int visibility) { int previousVisibility = getVisibility(); super.setVisibility(visibility); - if (viewEventListener != null - && previousVisibility == View.VISIBLE && visibility != View.VISIBLE) { - viewEventListener.onCloseSymbolInputView(); + if (viewEventListener.isPresent()) { + if (previousVisibility == View.VISIBLE && visibility != View.VISIBLE) { + viewEventListener.get().onCloseSymbolInputView(); + } else if (previousVisibility != View.VISIBLE && visibility == View.VISIBLE) { + viewEventListener.get().onShowSymbolInputView(Collections.emptyList()); + } } } void setSymbolCandidateStorage(SymbolCandidateStorage symbolCandidateStorage) { - this.symbolCandidateStorage = symbolCandidateStorage; + this.symbolCandidateStorage = Optional.of(symbolCandidateStorage); } void setKeyEventHandler(KeyEventHandler keyEventHandler) { + this.keyEventHandler = Optional.of(keyEventHandler); deleteKeyEventButtonTouchListener.setKeyEventHandler(keyEventHandler); + enterKeyEventButtonTouchListener.setKeyEventHandler(keyEventHandler); } void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) { @@ -923,12 +1024,19 @@ void setCandidateTextDimension(float candidateTextSize, float descriptionTextSiz this.desciptionTextSize = descriptionTextSize; } + void setPopupEnabled(boolean popupEnabled) { + this.popupEnabled = popupEnabled; + if (!isInflated()) { + return; + } + getNumberKeyboardView().setPopupEnabled(popupEnabled); + } + /** * Initializes EmojiProvider selection dialog, if necessary. - * Exposed as protected for testing purpose. */ - protected void maybeInitializeEmojiProviderDialog(Context context) { - if (emojiProviderDialog != null) { + @VisibleForTesting void maybeInitializeEmojiProviderDialog(Context context) { + if (emojiProviderDialog.isPresent()) { return; } @@ -938,7 +1046,7 @@ protected void maybeInitializeEmojiProviderDialog(Context context) { .setTitle(R.string.pref_emoji_provider_type_title) .setItems(R.array.pref_emoji_provider_type_entries, listener) .create(); - this.emojiProviderDialog = dialog; + this.emojiProviderDialog = Optional.of(dialog); } catch (InflateException e) { // Ignore the exception. } @@ -950,32 +1058,66 @@ protected void maybeInitializeEmojiProviderDialog(Context context) { * The view is updated. * The active minor category is also updated. * + * This method submit a preedit text except for a {@link SymbolMajorCategory#NUMBER} major + * category since this class commit a candidate directly. + * * @param newCategory the major category to show. */ - protected void setMajorCategory(SymbolMajorCategory newCategory) { - if (newCategory == null) { - throw new NullPointerException("newCategory must be non-null."); + @VisibleForTesting void setMajorCategory(SymbolMajorCategory newCategory) { + Preconditions.checkNotNull(newCategory); + + { + SymbolCandidateView symbolCandidateView = + SymbolCandidateView.class.cast(findViewById(R.id.symbol_input_candidate_view)); + if (symbolCandidateView != null) { + symbolCandidateView.reset(); + } + } + + if (newCategory != SymbolMajorCategory.NUMBER && viewEventListener.isPresent()) { + viewEventListener.get().onSubmitPreedit(); } + + if (newCategory == SymbolMajorCategory.NUMBER) { + CandidateView candidateView = + CandidateView.class.cast(findViewById(R.id.candidate_view_in_symbol_view)); + candidateView.clearAnimation(); + candidateView.setVisibility(View.GONE); + candidateView.reset(); + } + currentMajorCategory = newCategory; - // Reset the minor category to the default value. - resetTabText(); - resetCandidateViewPager(); - SymbolMinorCategory minorCategory = currentMajorCategory.getDefaultMinorCategory(); - if (symbolCandidateStorage.getCandidateList(minorCategory).getCandidatesCount() == 0) { - minorCategory = currentMajorCategory.getMinorCategoryByRelativeIndex(minorCategory, 1); + if (currentMajorCategory == SymbolMajorCategory.NUMBER) { + findViewById(android.R.id.tabhost).setVisibility(View.GONE); + findViewById(R.id.number_frame).setVisibility(View.VISIBLE); + setNumberKeyboard(); + } else { + findViewById(android.R.id.tabhost).setVisibility(View.VISIBLE); + findViewById(R.id.number_frame).setVisibility(View.GONE); + updateMinorCategory(); + } + + // Hide overlapping separator + if (currentMajorCategory == SymbolMajorCategory.NUMBER) { + findViewById(R.id.symbol_view_close_button_separator).setVisibility(View.INVISIBLE); + } else { + findViewById(R.id.symbol_view_close_button_separator).setVisibility(View.VISIBLE); + } + + if (currentMajorCategory == SymbolMajorCategory.EMOJI) { + findViewById(R.id.symbol_view_enter_button_separator).setVisibility(View.INVISIBLE); + } else { + findViewById(R.id.symbol_view_enter_button_separator).setVisibility(View.VISIBLE); } - int index = newCategory.minorCategories.indexOf(minorCategory); - getCandidateViewPager().setCurrentItem(index); - getTabHost().setCurrentTab(index); // Update visibility relating attributes. for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) { // Update major category selector button's look and feel. - ImageButton button = getMajorCategoryButton(majorCategory); + MozcImageButton button = getMajorCategoryButton(majorCategory); if (button != null) { - button.setSelected(majorCategory == newCategory); - button.setEnabled(majorCategory != newCategory); + button.setSelected(majorCategory == currentMajorCategory); + button.setEnabled(majorCategory != currentMajorCategory); } } @@ -983,15 +1125,62 @@ protected void setMajorCategory(SymbolMajorCategory newCategory) { if (emojiDisabledMessageView != null) { // Show messages about emoji-disabling, if necessary. emojiDisabledMessageView.setVisibility( - newCategory == SymbolMajorCategory.EMOJI && !emojiEnabled ? View.VISIBLE : View.GONE); + currentMajorCategory == SymbolMajorCategory.EMOJI + && !emojiEnabled ? View.VISIBLE : View.GONE); + } + } + + private void setNumberKeyboard() { + final KeyboardSpecification spec = KeyboardSpecification.SYMBOL_NUMBER; + final KeyboardFactory factory = new KeyboardFactory(); + + getNumberKeyboardView().addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange( + View view, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (right - left == 0 || bottom - top == 0) { + return; + } + KeyboardView keyboardView = KeyboardView.class.cast(view); + Keyboard keyboard = factory.get(getResources(), spec, right - left, bottom - top); + keyboardView.setKeyboard(keyboard); + keyboardView.invalidate(); + } + }); + } + + private void updateMinorCategory() { + // Reset the minor category to the default value. + resetTabImageForMinorCategory(); + resetCandidateViewPager(); + SymbolMinorCategory minorCategory = currentMajorCategory.getDefaultMinorCategory(); + Preconditions.checkState(symbolCandidateStorage.isPresent()); + if (symbolCandidateStorage.get().getCandidateList(minorCategory).getCandidatesCount() == 0) { + minorCategory = currentMajorCategory.getMinorCategoryByRelativeIndex(minorCategory, 1); } + int index = currentMajorCategory.minorCategories.indexOf(minorCategory); + getCandidateViewPager().setCurrentItem(index); + + // Disable feedback before setting the current tab programatically. + // Background: TabHost.setCurrentTab calls back onTabChanged, in which feedback event is fired. + // However, we don't have ways to distinguish if onTabChanged is called through user click + // event or by the call of setCurrentTab. If we don't disable feedback here, the click sound + // effect is fired twice; one is from the onClick event on major category tab and the other is + // by the call of setCurrentTab here. See b/17119766. + SymbolTabWidgetViewPagerAdapter adapter = + SymbolTabWidgetViewPagerAdapter.class.cast(getCandidateViewPager().getAdapter()); + adapter.setFeedbackEnabled(false); + getTabHost().setCurrentTab(index); + adapter.setFeedbackEnabled(true); } void setEmojiProviderType(EmojiProviderType emojiProviderType) { Preconditions.checkNotNull(emojiProviderType); + Preconditions.checkState(symbolCandidateStorage.isPresent()); this.emojiProviderType = emojiProviderType; - this.symbolCandidateStorage.setEmojiProviderType(emojiProviderType); + this.symbolCandidateStorage.get().setEmojiProviderType(emojiProviderType); if (!isInflated()) { return; } @@ -999,29 +1188,99 @@ void setEmojiProviderType(EmojiProviderType emojiProviderType) { resetCandidateViewPager(); } - void setViewEventListener(ViewEventListener listener, OnClickListener closeButtonClickListener) { - if (listener == null) { - throw new NullPointerException("lister must be non-null."); + void setEventListener( + ViewEventListener viewEventListener, OnClickListener closeButtonClickListener, + OnClickListener microphoneButtonClickListener) { + this.viewEventListener = Optional.of(viewEventListener); + this.closeButtonClickListener = Optional.of(closeButtonClickListener); + this.microphoneButtonClickListener = Optional.of(microphoneButtonClickListener); + } + + void setMicrophoneButtonEnabled(boolean enabled) { + isMicrophoneButtonEnabled = enabled; + if (isInflated()) { + getMicrophoneButton().setVisibility(enabled ? VISIBLE : GONE); } - viewEventListener = listener; - this.closeButtonClickListener = closeButtonClickListener; } - void setSkinType(SkinType skinType) { - if (this.skinType == skinType) { + void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + if (this.skin.equals(skin)) { return; } - - this.skinType = skinType; - mozcDrawableFactory.setSkinType(skinType); + this.skin = skin; + majorCategoryButtonDrawableFactory.setSkin(skin); if (!isInflated()) { return; } + updateSkinAwareDrawable(); + } - // Reset the minor category tab, candidate view and major category buttons. - resetTabBackground(); - resetCandidateViewPager(); - resetMajorCategoryBackground(); + @SuppressWarnings("deprecation") + private void updateSkinAwareDrawable() { + updateTabBackgroundSkin(); + resetTabImageForMinorCategory(); + + SymbolTabWidgetViewPagerAdapter adapter = + SymbolTabWidgetViewPagerAdapter.class.cast(getCandidateViewPager().getAdapter()); + if (adapter != null) { + adapter.setSkin(skin); + } + updateMajorCategoryBackgroundSkin(); + updateMajorCategoryButtonsSkin(); + updateMinorCategoryBackgroundSkin(); + updateNumberKeyboardSkin(); + updateSeparatorsSkin(); + getMicrophoneButton().setSkin(skin); + + TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs)); + for (int i = 0; i < tabWidget.getChildCount(); ++i) { + MozcImageView.class.cast(tabWidget.getChildTabViewAt(i)).setSkin(skin); + } + + // Note delete button shouldn't be applied createMajorButtonBackgroundDrawable as background + // as it should show different background (same as minor categories). + for (int id : new int[] {R.id.symbol_view_close_button, + R.id.symbol_view_enter_button}) { + MozcImageView view = MozcImageView.class.cast(findViewById(id)); + view.setSkin(skin); + view.setBackgroundDrawable(createMajorButtonBackgroundDrawable(skin)); + } + MozcImageView deleteKeyView = + MozcImageView.class.cast(findViewById(R.id.symbol_view_delete_button)); + deleteKeyView.setSkin(skin); + deleteKeyView.setBackgroundDrawable(createMinorButtonBackgroundDrawable(skin)); + } + + private KeyboardView getNumberKeyboardView() { + return KeyboardView.class.cast(findViewById(R.id.number_keyboard)); + } + + private LinearLayout getMajorCategoryFrame() { + return LinearLayout.class.cast(findViewById(R.id.symbol_major_category)); + } + + private LinearLayout getMinorCategoryFrame() { + return LinearLayout.class.cast(findViewById(R.id.symbol_minor_category)); + } + + private TabWidget getTabWidget() { + return TabWidget.class.cast(findViewById(android.R.id.tabs)); + } + + private MozcImageView getMicrophoneButton() { + return MozcImageView.class.cast(findViewById(R.id.microphone_button)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // The boundary of Drawable instance which has been set as background + // is not updated automatically. + // Update the boundary below. + if (isInflated()) { + updateSkinAwareDrawable(); + } } @Override diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java b/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java index c98b2f0cb..36cfcd19b 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java @@ -30,9 +30,9 @@ package org.mozc.android.inputmethod.japanese; import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; @@ -41,6 +41,8 @@ import java.util.List; +import javax.annotation.Nullable; + /** * This class delegates all method calls to a ViewEventListener, passed to the constructor. * Typical usage is to hook/override some of listener's methods to change their behavior. @@ -61,6 +63,7 @@ */ @SuppressWarnings("javadoc") public abstract class ViewEventDelegator implements ViewEventListener { + private final ViewEventListener delegated; public ViewEventDelegator(ViewEventListener delegated) { @@ -68,14 +71,15 @@ public ViewEventDelegator(ViewEventListener delegated) { } @Override - public void onKeyEvent(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface keyEvent, - KeyboardSpecification keyboardSpecification, - List touchEventList) { + public void onKeyEvent(@Nullable ProtoCommands.KeyEvent mozcKeyEvent, + @Nullable KeyEventInterface keyEvent, + @Nullable KeyboardSpecification keyboardSpecification, + List touchEventList) { delegated.onKeyEvent(mozcKeyEvent, keyEvent, keyboardSpecification, touchEventList); } @Override - public void onUndo(List touchEventList) { + public void onUndo(List touchEventList) { delegated.onUndo(touchEventList); } @@ -84,6 +88,16 @@ public void onConversionCandidateSelected(int candidateId, Optional row delegated.onConversionCandidateSelected(candidateId, Preconditions.checkNotNull(rowIndex)); } + @Override + public void onPageUp() { + delegated.onPageUp(); + } + + @Override + public void onPageDown() { + delegated.onPageDown(); + } + @Override public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String candidate, boolean updateHistory) { @@ -106,12 +120,12 @@ public void onExpandSuggestion() { } @Override - public void onShowMenuDialog(List touchEventList) { + public void onShowMenuDialog(List touchEventList) { delegated.onShowMenuDialog(touchEventList); } @Override - public void onShowSymbolInputView(List touchEventList) { + public void onShowSymbolInputView(List touchEventList) { delegated.onShowSymbolInputView(touchEventList); } @@ -129,4 +143,15 @@ public void onHardwareKeyboardCompositionModeChange(CompositionSwitchMode mode) public void onActionKey() { delegated.onActionKey(); } + + @Override + public void onNarrowModeChanged(boolean newNarrowMode) { + delegated.onNarrowModeChanged(newNarrowMode); + } + + @Override + public void onUpdateKeyboardLayoutAdjustment( + ViewManagerInterface.LayoutAdjustment layoutAdjustment) { + delegated.onUpdateKeyboardLayoutAdjustment(layoutAdjustment); + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java b/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java index e43e4a627..c7cf44150 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java @@ -30,9 +30,9 @@ package org.mozc.android.inputmethod.japanese; import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; @@ -40,6 +40,8 @@ import java.util.List; +import javax.annotation.Nullable; + /** * Callback object for view evnets. * @@ -54,16 +56,17 @@ public interface ViewEventListener { * @param touchEventList {@code TouchEvent} instances related to this key event for logging * usage stats. */ - public void onKeyEvent(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface keyEvent, - KeyboardSpecification keyboardSpecification, - List touchEventList); + public void onKeyEvent(@Nullable ProtoCommands.KeyEvent mozcKeyEvent, + @Nullable KeyEventInterface keyEvent, + @Nullable KeyboardSpecification keyboardSpecification, + List touchEventList); /** * Called when Undo is fired (by soft keyboard). * @param touchEventList {@code TouchEvent} instances related to this undo for logging * usage stats. */ - public void onUndo(List touchEventList); + public void onUndo(List touchEventList); /** * Called when a conversion candidate is selected. @@ -73,6 +76,12 @@ public void onKeyEvent(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface ke */ public void onConversionCandidateSelected(int candidateId, Optional rowIndex); + /** Called when page down button is tapped. */ + public void onPageUp(); + + /** Called when page down button is tapped. */ + public void onPageDown(); + /** * Called when a candidate on symbol input view is selected. */ @@ -102,7 +111,7 @@ public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String * for logging usage stats. */ // TODO(matsuzakit): Rename. onFlushTouchEventStats ? - public void onShowMenuDialog(List touchEventList); + public void onShowMenuDialog(List touchEventList); /** * Called when the symbol input view is shown. @@ -110,7 +119,7 @@ public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String * @param touchEventList {@code TouchEvent} instances which is related to this event * for logging usage stats. */ - public void onShowSymbolInputView(List touchEventList); + public void onShowSymbolInputView(List touchEventList); /** * Called when the symbol input view is closed. @@ -127,4 +136,15 @@ public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String * Called when the key for editor action is pressed. */ public void onActionKey(); + + /** Called when the narrow mode of the view is changed. */ + public void onNarrowModeChanged(boolean newNarrowMode); + + /** + * Called when the keyboard layout preference should be updated. + *

+ * The visible keyboard will also be updated as the result through a callback object. + */ + public void onUpdateKeyboardLayoutAdjustment( + ViewManagerInterface.LayoutAdjustment layoutAdjustment); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java b/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java index ea0f2b8e0..a3927cc85 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java @@ -30,18 +30,24 @@ package org.mozc.android.inputmethod.japanese; import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; import org.mozc.android.inputmethod.japanese.emoji.EmojiUtil; +import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard; +import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; import org.mozc.android.inputmethod.japanese.keyboard.KeyEntity; import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener; +import org.mozc.android.inputmethod.japanese.keyboard.KeyboardFactory; import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser; import org.mozc.android.inputmethod.japanese.model.JapaneseSoftwareKeyboardModel; import org.mozc.android.inputmethod.japanese.model.JapaneseSoftwareKeyboardModel.KeyboardMode; import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage; import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage.SymbolHistoryStorage; +import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; +import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.InputStyle; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; @@ -52,11 +58,12 @@ import org.mozc.android.inputmethod.japanese.ui.MenuDialog; import org.mozc.android.inputmethod.japanese.ui.MenuDialog.MenuDialogListener; import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -65,6 +72,7 @@ import android.os.Build; import android.os.IBinder; import android.os.Looper; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -72,6 +80,7 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import java.util.Collections; @@ -89,6 +98,7 @@ public class ViewManager implements ViewManagerInterface { * An small wrapper to inject keyboard view resizing when a user selects a candidate. */ class ViewManagerEventListener extends ViewEventDelegator { + ViewManagerEventListener(ViewEventListener delegated) { super(delegated); } @@ -105,37 +115,32 @@ public void onConversionCandidateSelected(int candidateId, Optional row /** * Converts S/W Keyboard's keycode to KeyEvent instance. - * Exposed as protected for testing purpose. */ - protected void onKey(int primaryCode, List touchEventList) { - if (primaryCode == keycodeCapslock || - primaryCode == keycodeAlt) { + void onKey(int primaryCode, List touchEventList) { + if (primaryCode == keycodeCapslock || primaryCode == keycodeAlt) { // Ignore those key events because they are handled by KeyboardView, // but send touchEventList for logging usage stats. - if (eventListener != null) { - eventListener.onKeyEvent(null, null, null, touchEventList); - } + eventListener.onKeyEvent(null, null, null, touchEventList); return; } // Keyboard switch event. - if (primaryCode == keycodeChartypeToKana || - primaryCode == keycodeChartypeTo123 || - primaryCode == keycodeChartypeToAbc || - primaryCode == keycodeChartypeToKana123 || - primaryCode == keycodeChartypeToAbc123) { + if (primaryCode == keycodeChartypeToKana + || primaryCode == keycodeChartypeToAbc + || primaryCode == keycodeChartypeToAbc123) { if (primaryCode == keycodeChartypeToKana) { japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.KANA); } else if (primaryCode == keycodeChartypeToAbc) { japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.ALPHABET); - } else if (primaryCode == keycodeChartypeTo123 || - primaryCode == keycodeChartypeToKana123) { - japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.KANA_NUMBER); } else if (primaryCode == keycodeChartypeToAbc123) { japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.ALPHABET_NUMBER); } - setJapaneseKeyboard( - japaneseSoftwareKeyboardModel.getKeyboardSpecification(), touchEventList); + propagateSoftwareKeyboardChange(touchEventList); + return; + } + + if (primaryCode == keycodeGlobe) { + imeSwitcher.switchToNextInputMethod(false); return; } @@ -144,9 +149,7 @@ protected void onKey(int primaryCode, List touchEventList) if (mozcView != null) { mozcView.resetKeyboardViewState(); } - if (eventListener != null) { - eventListener.onShowMenuDialog(touchEventList); - } + eventListener.onShowMenuDialog(touchEventList); if (primaryCode == keycodeMenuDialog) { showMenuDialog(); } else if (primaryCode == keycodeImePickerDialog) { @@ -156,34 +159,30 @@ protected void onKey(int primaryCode, List touchEventList) } if (primaryCode == keycodeSymbol) { - if (eventListener != null) { - eventListener.onSubmitPreedit(); + if (mozcView != null) { + mozcView.showSymbolInputView(Optional.absent()); } + return; + } + + if (primaryCode == keycodeSymbolEmoji) { if (mozcView != null) { - mozcView.resetKeyboardViewState(); - mozcView.showSymbolInputView(); - if (eventListener != null) { - eventListener.onShowSymbolInputView(touchEventList); - } + mozcView.showSymbolInputView(Optional.of(SymbolMajorCategory.EMOJI)); } return; } if (primaryCode == keycodeUndo) { - if (eventListener != null) { - eventListener.onUndo(touchEventList); - } + eventListener.onUndo(touchEventList); return; } - ProtoCommands.KeyEvent mozcKeyEvent = + Optional mozcKeyEvent = primaryKeyCodeConverter.createMozcKeyEvent(primaryCode, touchEventList); - if (eventListener != null) { - eventListener.onKeyEvent(mozcKeyEvent, - primaryKeyCodeConverter.getPrimaryCodeKeyEvent(primaryCode), - japaneseSoftwareKeyboardModel.getKeyboardSpecification(), - touchEventList); - } + eventListener.onKeyEvent(mozcKeyEvent.orNull(), + primaryKeyCodeConverter.getPrimaryCodeKeyEvent(primaryCode), + getActiveSoftwareKeyboardModel().getKeyboardSpecification(), + touchEventList); } /** @@ -196,13 +195,13 @@ public void onCancel() { } @Override - public void onKey(int primaryCode, List touchEventList) { + public void onKey(int primaryCode, List touchEventList) { ViewManager.this.onKey(primaryCode, touchEventList); } @Override public void onPress(int primaryCode) { - if (eventListener != null && primaryCode != KeyEntity.INVALID_KEY_CODE) { + if (primaryCode != KeyEntity.INVALID_KEY_CODE) { eventListener.onFireFeedbackEvent(FeedbackEvent.KEY_DOWN); } } @@ -212,6 +211,92 @@ public void onRelease(int primaryCode) { } } + @VisibleForTesting class ViewLayerEventHandler { + private static final int NEXUS_KEYBOARD_VENDOR_ID = 0x0D62; + private static final int NEXUS_KEYBOARD_PRODUCT_ID = 0x160B; + private boolean isEmojiKeyDownAvailable = false; + private boolean isEmojiInvoking = false; + private int pressedKeyNum = 0; + @VisibleForTesting boolean disableDeviceCheck = false; + + @SuppressLint("NewApi") + private boolean hasPhysicalEmojiKey(KeyEvent event) { + InputDevice device = InputDevice.getDevice(event.getDeviceId()); + return disableDeviceCheck + || (Build.VERSION.SDK_INT >= 19 + && device != null + && device.getVendorId() == NEXUS_KEYBOARD_VENDOR_ID + && device.getProductId() == NEXUS_KEYBOARD_PRODUCT_ID); + } + + private boolean isEmojiKey(KeyEvent event) { + if (!hasPhysicalEmojiKey(event)) { + return false; + } + if (event.getKeyCode() != KeyEvent.KEYCODE_ALT_LEFT + && event.getKeyCode() != KeyEvent.KEYCODE_ALT_RIGHT) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_UP) { + return event.hasNoModifiers(); + } else { + return event.hasModifiers(KeyEvent.META_ALT_ON); + } + } + + public boolean evaluateKeyEvent(KeyEvent event) { + Preconditions.checkNotNull(event); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + ++pressedKeyNum; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + pressedKeyNum = Math.max(0, pressedKeyNum - 1); + } else { + return false; + } + + if (isEmojiKey(event)) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + isEmojiKeyDownAvailable = true; + isEmojiInvoking = false; + } else if (isEmojiKeyDownAvailable && pressedKeyNum == 0) { + isEmojiKeyDownAvailable = false; + isEmojiInvoking = true; + } + } else { + isEmojiKeyDownAvailable = false; + isEmojiInvoking = false; + } + return isEmojiInvoking; + } + + public void invoke() { + if (!isEmojiInvoking) { + return; + } + isEmojiInvoking = false; + if (mozcView != null) { + if (isSymbolInputViewVisible) { + mozcView.hideSymbolInputView(); + if (!isNarrowMode()) { + setNarrowMode(true); + } + } else { + isSymbolInputViewShownByEmojiKey = true; + if (isNarrowMode()) { + setNarrowMode(false); + } + mozcView.showSymbolInputView(Optional.of(SymbolMajorCategory.EMOJI)); + } + } + } + + public void reset() { + isEmojiKeyDownAvailable = false; + isEmojiInvoking = false; + pressedKeyNum = 0; + } + } + // Registered by the user (typically MozcService) @VisibleForTesting final ViewEventListener eventListener; @@ -225,38 +310,83 @@ public void onRelease(int primaryCode) { // IME switcher instance to detect that voice input is available or not. private final ImeSwitcher imeSwitcher; - // Called back by keyboards. + /** Key event handler to handle events on Mozc server. */ private final KeyEventHandler keyEventHandler; - // Model to represent the current software keyboard state. - @VisibleForTesting final JapaneseSoftwareKeyboardModel japaneseSoftwareKeyboardModel = + /** Key event handler to handle events on view layer. */ + @VisibleForTesting final ViewLayerEventHandler viewLayerKeyEventHandler = + new ViewLayerEventHandler(); + + /** + * Model to represent the current software keyboard state. + * All the setter methods don't affect symbolNumberSoftwareKeyboardModel but + * japaneseSoftwareKeyboardModel. + */ + private final JapaneseSoftwareKeyboardModel japaneseSoftwareKeyboardModel = new JapaneseSoftwareKeyboardModel(); + /** + * Model to represent the number software keyboard state. + * Its keyboard mode is set in the constructor to KeyboardMode.SYMBOL_NUMBER and will never be + * changed. + */ + private final JapaneseSoftwareKeyboardModel symbolNumberSoftwareKeyboardModel = + new JapaneseSoftwareKeyboardModel(); + + @VisibleForTesting final HardwareKeyboard hardwareKeyboard; + + /** True if symbol input view is visible. */ + private boolean isSymbolInputViewVisible; - // The factory of parsed keyboard data. - private final JapaneseKeyboardFactory japaneseKeyboardFactory = new JapaneseKeyboardFactory(); + /** True if symbol input view is shown by the Emoji key on physical keyboard. */ + private boolean isSymbolInputViewShownByEmojiKey; + + /** The factory of parsed keyboard data. */ + private final KeyboardFactory keyboardFactory = new KeyboardFactory(); private final SymbolCandidateStorage symbolCandidateStorage; - // Current fullscreen mode + /** Current fullscreen mode */ private boolean fullscreenMode = false; - // Current narrow mode + /** Current narrow mode */ private boolean narrowMode = false; - // Current popup enabled state. + /** Current popup enabled state. */ private boolean popupEnabled = true; - // Current voice input allowed state. - private boolean voiceInputAllowed = false; + /** Current Globe button enabled state. */ + private boolean globeButtonEnabled = false; - private int flickSensitivity = 0; + /** True if CursorAnchorInfo is enabled. */ + private boolean cursorAnchroInfoEnabled = false; + + /** True if hardware keyboard exists. */ + private boolean hardwareKeyboardExist = false; + + /** + * True if voice input is eligible. + *

+ * This conditions is calculated based on following conditions. + *

    + *
  • VoiceIME's status: If VoiceIME is not available, this flag becomes false. + *
  • EditorInfo: If current editor does not want to use voice input, this flag becomes false. + *
      + *
    • Voice input might be explicitly forbidden by the editor. + *
    • Voice input should be useless for the number input editors. + *
    • Voice input should be useless for password field. + *
        + *
      + */ + private boolean isVoiceInputEligible = false; - private CompositionMode hardwareCompositionMode = CompositionMode.HIRAGANA; + private boolean isVoiceInputEnabledByPreference = true; + + private int flickSensitivity = 0; @VisibleForTesting EmojiProviderType emojiProviderType = EmojiProviderType.NONE; /** Current skin type. */ - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; + private Skin skin = Skin.getFallbackInstance(); private LayoutAdjustment layoutAdjustment = LayoutAdjustment.FILL; @@ -269,47 +399,51 @@ public void onRelease(int primaryCode) { // but such name like "KEYCODE_LEFT" makes Lint unhappy // because they are not "static final". private final int keycodeChartypeToKana; - private final int keycodeChartypeTo123; private final int keycodeChartypeToAbc; - private final int keycodeChartypeToKana123; private final int keycodeChartypeToAbc123; + private final int keycodeGlobe; private final int keycodeSymbol; + private final int keycodeSymbolEmoji; private final int keycodeUndo; private final int keycodeCapslock; private final int keycodeAlt; private final int keycodeMenuDialog; private final int keycodeImePickerDialog; - // Handles software keyboard event and sends it to the service. + /** Handles software keyboard event and sends it to the service. */ private final KeyboardActionAdapter keyboardActionListener; private final PrimaryKeyCodeConverter primaryKeyCodeConverter; - public ViewManager(Context context, final ViewEventListener listener, + public ViewManager(Context context, ViewEventListener listener, SymbolHistoryStorage symbolHistoryStorage, ImeSwitcher imeSwitcher, MenuDialogListener menuDialogListener) { this(context, listener, symbolHistoryStorage, imeSwitcher, menuDialogListener, - new ProbableKeyEventGuesser(context.getAssets())); + new ProbableKeyEventGuesser(context.getAssets()), new HardwareKeyboard()); } @VisibleForTesting ViewManager(Context context, ViewEventListener listener, SymbolHistoryStorage symbolHistoryStorage, ImeSwitcher imeSwitcher, - @Nullable MenuDialogListener menuDialogListener, ProbableKeyEventGuesser guesser) { + @Nullable MenuDialogListener menuDialogListener, ProbableKeyEventGuesser guesser, + HardwareKeyboard hardwareKeyboard) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(listener); Preconditions.checkNotNull(imeSwitcher); + Preconditions.checkNotNull(hardwareKeyboard); primaryKeyCodeConverter = new PrimaryKeyCodeConverter(context, guesser); + symbolNumberSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.SYMBOL_NUMBER); + // Prefetch keycodes from resource Resources res = context.getResources(); keycodeChartypeToKana = res.getInteger(R.integer.key_chartype_to_kana); - keycodeChartypeTo123 = res.getInteger(R.integer.key_chartype_to_123); keycodeChartypeToAbc = res.getInteger(R.integer.key_chartype_to_abc); - keycodeChartypeToKana123 = res.getInteger(R.integer.key_chartype_to_kana_123); keycodeChartypeToAbc123 = res.getInteger(R.integer.key_chartype_to_abc_123); + keycodeGlobe = res.getInteger(R.integer.key_globe); keycodeSymbol = res.getInteger(R.integer.key_symbol); + keycodeSymbolEmoji = res.getInteger(R.integer.key_symbol_emoji); keycodeUndo = res.getInteger(R.integer.key_undo); keycodeCapslock = res.getInteger(R.integer.key_capslock); keycodeAlt = res.getInteger(R.integer.key_alt); @@ -330,6 +464,7 @@ public ViewManager(Context context, final ViewEventListener listener, this.imeSwitcher = imeSwitcher; this.menuDialogListener = menuDialogListener; this.symbolCandidateStorage = new SymbolCandidateStorage(symbolHistoryStorage); + this.hardwareKeyboard = hardwareKeyboard; } /** @@ -356,35 +491,46 @@ public MozcView createMozcView(Context context) { // until all the updates done in this method are finished. Just in case. mozcView.setVisibility(View.GONE); mozcView.setKeyboardHeightRatio(keyboardHeightRatio); + mozcView.setCursorAnchorInfoEnabled(cursorAnchroInfoEnabled); + OnClickListener widenButtonClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + eventListener.onFireFeedbackEvent(FeedbackEvent.NARROW_FRAME_WIDEN_BUTTON_DOWN); + setNarrowMode(!narrowMode); + } + }; + OnClickListener leftAdjustButtonClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + eventListener.onUpdateKeyboardLayoutAdjustment(LayoutAdjustment.LEFT); + } + }; + OnClickListener rightAdjustButtonClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + eventListener.onUpdateKeyboardLayoutAdjustment(LayoutAdjustment.RIGHT); + } + }; + + OnClickListener microphoneButtonClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + eventListener.onFireFeedbackEvent(FeedbackEvent.MICROPHONE_BUTTON_DOWN); + imeSwitcher.switchToVoiceIme("ja-jp"); + } + }; mozcView.setEventListener( eventListener, - new OnClickListener() { - @Override - public void onClick(View v) { - setNarrowMode(!narrowMode); - } - }, + widenButtonClickListener, // User pushes these buttons to move position in order to see hidden text in editing rather // than to change his/her favorite position. So we should not apply it to preferences. - new OnClickListener() { - @Override - public void onClick(View v) { - setLayoutAdjustment(v.getContext().getResources(), LayoutAdjustment.LEFT); - mozcView.startLayoutAdjustmentAnimation(); - } - }, - new OnClickListener() { - @Override - public void onClick(View v) { - setLayoutAdjustment(v.getContext().getResources(), LayoutAdjustment.RIGHT); - mozcView.startLayoutAdjustmentAnimation(); - } - }); + leftAdjustButtonClickListener, + rightAdjustButtonClickListener, + microphoneButtonClickListener); mozcView.setKeyEventHandler(keyEventHandler); - setJapaneseKeyboard(japaneseSoftwareKeyboardModel.getKeyboardSpecification(), - Collections.emptyList()); + propagateSoftwareKeyboardChange(Collections.emptyList()); mozcView.setFullscreenMode(fullscreenMode); mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode); // At the moment, it is necessary to set the storage to the view, *before* setting emoji @@ -392,10 +538,9 @@ public void onClick(View v) { // TODO(hidehiko): Remove the restriction. mozcView.setSymbolCandidateStorage(symbolCandidateStorage); mozcView.setEmojiProviderType(emojiProviderType); - mozcView.setHardwareCompositionButtonImage(hardwareCompositionMode); mozcView.setPopupEnabled(popupEnabled); mozcView.setFlickSensitivity(flickSensitivity); - mozcView.setSkinType(skinType); + mozcView.setSkin(skin); // Clear the menu dialog. menuDialog = null; @@ -412,9 +557,7 @@ private void showMenuDialog() { return; } - boolean voiceInputEnabled = voiceInputAllowed && imeSwitcher.isVoiceImeAvailable(); - menuDialog = new MenuDialog( - mozcView.getContext(), Optional.fromNullable(menuDialogListener), voiceInputEnabled); + menuDialog = new MenuDialog(mozcView.getContext(), Optional.fromNullable(menuDialogListener)); IBinder windowToken = mozcView.getWindowToken(); if (windowToken == null) { MozcLog.w("Unknown window token"); @@ -454,8 +597,8 @@ public void render(Command outCommand) { if (mozcView == null) { return; } - if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() == 0 && - !outCommand.getInput().getRequestSuggestion()) { + if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() == 0 + && !outCommand.getInput().getRequestSuggestion()) { // The server doesn't return the suggestion result, because there is following // key sequence, which will trigger the suggest and the new suggestion will overwrite // the current suggest. In order to avoid chattering the candidate window, @@ -475,26 +618,32 @@ public void render(Command outCommand) { * @return the current keyboard specification. */ @Override - public KeyboardSpecification getJapaneseKeyboardSpecification() { - return japaneseSoftwareKeyboardModel.getKeyboardSpecification(); + public KeyboardSpecification getKeyboardSpecification() { + return getActiveSoftwareKeyboardModel().getKeyboardSpecification(); } - /** - * Set {@code EditorInfo} instance to the current view. - */ + /** Set {@code EditorInfo} instance to the current view. */ @Override public void setEditorInfo(EditorInfo attribute) { - mozcView.setEmojiEnabled( - EmojiUtil.isUnicodeEmojiAvailable(Build.VERSION.SDK_INT), - EmojiUtil.isCarrierEmojiAllowed(attribute)); - mozcView.setPasswordField(MozcUtil.isPasswordField(attribute)); - mozcView.setEditorInfo(attribute); - voiceInputAllowed = MozcUtil.isVoiceInputAllowed(attribute); + if (mozcView != null) { + mozcView.setEmojiEnabled( + EmojiUtil.isUnicodeEmojiAvailable(Build.VERSION.SDK_INT), + EmojiUtil.isCarrierEmojiAllowed(attribute)); + mozcView.setPasswordField(MozcUtil.isPasswordField(attribute.inputType)); + mozcView.setEditorInfo(attribute); + } + isVoiceInputEligible = MozcUtil.isVoiceInputPreferred(attribute); japaneseSoftwareKeyboardModel.setInputType(attribute.inputType); - setJapaneseKeyboard( - japaneseSoftwareKeyboardModel.getKeyboardSpecification(), - Collections.emptyList()); + // TODO(hsumita): Set input type on Hardware keyboard, too. Otherwise, Hiragana input can be + // enabled unexpectedly. (e.g. Number text field.) + propagateSoftwareKeyboardChange(Collections.emptyList()); + } + + private boolean shouldVoiceImeBeEnabled() { + // Disable voice IME if hardware keyboard exists to avoid a framework bug. + return isVoiceInputEligible && isVoiceInputEnabledByPreference && !hardwareKeyboardExist + && imeSwitcher.isVoiceImeAvailable(); } @Override @@ -509,6 +658,12 @@ public boolean hideSubInputView() { } MozcView mozcView = this.mozcView; + if (isSymbolInputViewShownByEmojiKey) { + setNarrowMode(true); + mozcView.hideSymbolInputView(); + return true; + } + // Try to hide a sub view from front to back. if (mozcView.hideSymbolInputView()) { return true; @@ -519,22 +674,66 @@ public boolean hideSubInputView() { /** * Creates and sets a keyboard represented by the resource id to the input frame. - * + *

      * Note that this method requires inputFrameView is not null, and its first child is * the JapaneseKeyboardView. - * @param specification Keyboard specification for the next */ - private void setJapaneseKeyboard( - KeyboardSpecification specification, List touchEventList) { + private void updateKeyboardView() { + if (mozcView == null) { + return; + } + Rect size = mozcView.getKeyboardSize(); + Keyboard keyboard = keyboardFactory.get( + mozcView.getResources(), japaneseSoftwareKeyboardModel.getKeyboardSpecification(), + size.width(), size.height()); + mozcView.setKeyboard(keyboard); + primaryKeyCodeConverter.setKeyboard(keyboard); + } + + /** + * Propagates the change of S/W keyboard to the view layer and the H/W keyboard configuration. + */ + private void propagateSoftwareKeyboardChange(List touchEventList) { + KeyboardSpecification specification = japaneseSoftwareKeyboardModel.getKeyboardSpecification(); + + // TODO(team): The purpose of the following call of onKeyEvent() is to tell the change of + // software keyboard specification to Mozc server through the event listener registered by + // MozcService. Obviously, calling onKeyEvent() for this purpose is abuse and should be fixed. eventListener.onKeyEvent(null, null, specification, touchEventList); - if (mozcView != null) { - Rect size = mozcView.getKeyboardSize(); - JapaneseKeyboard japaneseKeyboard = - japaneseKeyboardFactory.get(mozcView.getResources(), specification, - size.width(), size.height()); - mozcView.setJapaneseKeyboard(japaneseKeyboard); - primaryKeyCodeConverter.setJapaneseKeyboard(japaneseKeyboard); + + // Update H/W keyboard specification to keep a consistency with S/W keyboard. + hardwareKeyboard.setCompositionMode( + specification.getCompositionMode() == CompositionMode.HIRAGANA + ? CompositionSwitchMode.KANA : CompositionSwitchMode.ALPHABET); + + updateKeyboardView(); + } + + private void propagateHardwareKeyboardChange() { + propagateHardwareKeyboardChangeAndSendKey(null); + } + + /** + * Propagates the change of S/W keyboard to the view layer and the H/W keyboard configuration, and + * the send key event to Mozc server. + */ + private void propagateHardwareKeyboardChangeAndSendKey(@Nullable KeyEvent event) { + KeyboardSpecification specification = hardwareKeyboard.getKeyboardSpecification(); + + if (event == null) { + eventListener.onKeyEvent(null, null, specification, Collections.emptyList()); + } else { + eventListener.onKeyEvent( + hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event), + specification, Collections.emptyList()); } + + // Update S/W keyboard specification to keep a consistency with H/W keyboard. + japaneseSoftwareKeyboardModel.setKeyboardMode( + specification.getCompositionMode() == CompositionMode.HIRAGANA + ? KeyboardMode.KANA : KeyboardMode.ALPHABET); + + updateKeyboardView(); } /** @@ -548,13 +747,11 @@ public void setKeyboardLayout(KeyboardLayout keyboardLayout) { if (japaneseSoftwareKeyboardModel.getKeyboardLayout() != keyboardLayout) { // If changed, clear the keyboard cache. - japaneseKeyboardFactory.clear(); + keyboardFactory.clear(); } japaneseSoftwareKeyboardModel.setKeyboardLayout(keyboardLayout); - setJapaneseKeyboard( - japaneseSoftwareKeyboardModel.getKeyboardSpecification(), - Collections.emptyList()); + propagateSoftwareKeyboardChange(Collections.emptyList()); } /** @@ -569,26 +766,22 @@ public void setInputStyle(InputStyle inputStyle) { if (japaneseSoftwareKeyboardModel.getInputStyle() != inputStyle) { // If changed, clear the keyboard cache. - japaneseKeyboardFactory.clear(); + keyboardFactory.clear(); } japaneseSoftwareKeyboardModel.setInputStyle(inputStyle); - setJapaneseKeyboard( - japaneseSoftwareKeyboardModel.getKeyboardSpecification(), - Collections.emptyList()); + propagateSoftwareKeyboardChange(Collections.emptyList()); } @Override public void setQwertyLayoutForAlphabet(boolean qwertyLayoutForAlphabet) { if (japaneseSoftwareKeyboardModel.isQwertyLayoutForAlphabet() != qwertyLayoutForAlphabet) { // If changed, clear the keyboard cache. - japaneseKeyboardFactory.clear(); + keyboardFactory.clear(); } japaneseSoftwareKeyboardModel.setQwertyLayoutForAlphabet(qwertyLayoutForAlphabet); - setJapaneseKeyboard( - japaneseSoftwareKeyboardModel.getKeyboardSpecification(), - Collections.emptyList()); + propagateSoftwareKeyboardChange(Collections.emptyList()); } @Override @@ -623,13 +816,41 @@ public void setEmojiProviderType(EmojiProviderType emojiProviderType) { } /** - * @param isNarrowMode Whether mozc view shows in narrow mode or normal. + * Updates whether Globe button should be enabled or not based on + * {@code InputMethodManager#shouldOfferSwitchingToNextInputMethod(IBinder)} + */ + @Override + public void updateGlobeButtonEnabled() { + this.globeButtonEnabled = imeSwitcher.shouldOfferSwitchingToNextInputMethod(); + if (mozcView != null) { + mozcView.setGlobeButtonEnabled(globeButtonEnabled); + } + } + + /** + * Updates whether Microphone button should be enabled or not based on + * availability of voice input method. + */ + @Override + public void updateMicrophoneButtonEnabled() { + if (mozcView != null) { + mozcView.setMicrophoneButtonEnabled(shouldVoiceImeBeEnabled()); + } + } + + /** + * @param newNarrowMode Whether mozc view shows in narrow mode or normal. */ @Override - public void setNarrowMode(boolean isNarrowMode) { - this.narrowMode = isNarrowMode; + public void setNarrowMode(boolean newNarrowMode) { + boolean previousNarrowMode = this.narrowMode; + this.narrowMode = newNarrowMode; if (mozcView != null) { - mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, isNarrowMode); + mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, newNarrowMode); + } + updateMicrophoneButtonEnabled(); + if (previousNarrowMode != newNarrowMode) { + eventListener.onNarrowModeChanged(newNarrowMode); } } @@ -646,20 +867,19 @@ public void setNarrowMode(boolean isNarrowMode) { @Override public void maybeTransitToNarrowMode(Command command, KeyEventInterface keyEventInterface) { Preconditions.checkNotNull(command); - // Surely we don't anthing when on narrow mode already. + // Surely we don't anything when on narrow mode already. if (isNarrowMode()) { return; } // Do nothing for the input from software keyboard. - if (keyEventInterface == null || keyEventInterface.getNativeEvent() == null) { + if (keyEventInterface == null || !keyEventInterface.getNativeEvent().isPresent()) { return; } - // Do nothing if the key event doesn't have printable character without modifier. - if (!command.getInput().hasKey() - || !command.getInput().getKey().hasKeyCode() - || command.getInput().getKey().hasModifiers()) { + // Do nothing if input doesn't have a key. (e.g. pure modifier key) + if (!command.getInput().hasKey()) { return; } + // Passed all the check. Transit to narrow mode. hideSubInputView(); setNarrowMode(true); @@ -670,6 +890,11 @@ public boolean isNarrowMode() { return narrowMode; } + @Override + public boolean isFloatingCandidateMode() { + return mozcView != null && mozcView.isFloatingCandidateMode(); + } + @Override public void setPopupEnabled(boolean popupEnabled) { this.popupEnabled = popupEnabled; @@ -679,28 +904,53 @@ public void setPopupEnabled(boolean popupEnabled) { } @Override - public void setHardwareKeyboardCompositionMode(CompositionMode compositionMode) { - hardwareCompositionMode = compositionMode; - if (mozcView != null) { - mozcView.setHardwareCompositionButtonImage(compositionMode); + public void switchHardwareKeyboardCompositionMode(CompositionSwitchMode mode) { + Preconditions.checkNotNull(mode); + + CompositionMode oldMode = hardwareKeyboard.getCompositionMode(); + hardwareKeyboard.setCompositionMode(mode); + CompositionMode newMode = hardwareKeyboard.getCompositionMode(); + if (oldMode != newMode) { + propagateHardwareKeyboardChange(); } } @Override - public void setSkinType(SkinType skinType) { - this.skinType = skinType; + public void setHardwareKeyMap(HardwareKeyMap hardwareKeyMap) { + hardwareKeyboard.setHardwareKeyMap(Preconditions.checkNotNull(hardwareKeyMap)); + } + + @Override + public void setSkin(Skin skin) { + this.skin = Preconditions.checkNotNull(skin); if (mozcView != null) { - mozcView.setSkinType(skinType); + mozcView.setSkin(skin); } } @Override - public void setLayoutAdjustment(Resources resources, LayoutAdjustment layoutAdjustment) { - this.layoutAdjustment = layoutAdjustment; + public void setMicrophoneButtonEnabledByPreference(boolean microphoneButtonEnabled) { + this.isVoiceInputEnabledByPreference = microphoneButtonEnabled; + updateMicrophoneButtonEnabled(); + } + /** + * Set layout adjustment and show animation if required. + *

      + * Note that this method does *NOT* update SharedPreference. + * If you want to update it, use ViewEventListener#onUpdateKeyboardLayoutAdjustment(), + * which updates SharedPreference and indirectly calls this method. + */ + @Override + public void setLayoutAdjustment(LayoutAdjustment layoutAdjustment) { + Preconditions.checkNotNull(layoutAdjustment); if (mozcView != null) { mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode); + if (this.layoutAdjustment != layoutAdjustment) { + mozcView.startLayoutAdjustmentAnimation(); + } } + this.layoutAdjustment = layoutAdjustment; } @Override @@ -724,6 +974,8 @@ public void reset() { mozcView.reset(); } + viewLayerKeyEventHandler.reset(); + // Reset menu dialog. maybeDismissMenuDialog(); } @@ -771,16 +1023,33 @@ public void computeInsets(Context context, InputMethodService.Insets outInsets, @Override public void onConfigurationChanged(Configuration newConfig) { primaryKeyCodeConverter.setConfiguration(newConfig); + hardwareKeyboardExist = newConfig.keyboard != Configuration.KEYBOARD_NOKEYS; } @Override public boolean isKeyConsumedOnViewAsynchronously(KeyEvent event) { - return false; + return viewLayerKeyEventHandler.evaluateKeyEvent(Preconditions.checkNotNull(event)); } @Override public void consumeKeyOnViewSynchronously(KeyEvent event) { - throw new IllegalArgumentException("ViewManager doesn't consume any key event."); + viewLayerKeyEventHandler.invoke(); + } + + @Override + public void onHardwareKeyEvent(KeyEvent event) { + // Maybe update the composition mode based on the event. + // For example, zen/han key toggles the composition mode (hiragana <--> alphabet). + CompositionMode compositionMode = hardwareKeyboard.getCompositionMode(); + hardwareKeyboard.setCompositionModeByKey(event); + CompositionMode currentCompositionMode = hardwareKeyboard.getCompositionMode(); + if (compositionMode != currentCompositionMode) { + propagateHardwareKeyboardChangeAndSendKey(event); + } else { + eventListener.onKeyEvent( + hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event), + hardwareKeyboard.getKeyboardSpecification(), Collections.emptyList()); + } } @Override @@ -799,10 +1068,18 @@ public ViewEventListener getEventListener() { return eventListener; } + /** + * Returns active (shown) JapaneseSoftwareKeyboardModel. + * If symbol picker is shown, symbol-number keyboard's is returned. + */ @VisibleForTesting @Override - public JapaneseSoftwareKeyboardModel getJapaneseSoftwareKeyboardModel() { - return japaneseSoftwareKeyboardModel; + public JapaneseSoftwareKeyboardModel getActiveSoftwareKeyboardModel() { + if (isSymbolInputViewVisible) { + return symbolNumberSoftwareKeyboardModel; + } else { + return japaneseSoftwareKeyboardModel; + } } @VisibleForTesting @@ -825,8 +1102,14 @@ public EmojiProviderType getEmojiProviderType() { @VisibleForTesting @Override - public SkinType getSkinType() { - return skinType; + public Skin getSkin() { + return skin; + } + + @VisibleForTesting + @Override + public boolean isMicrophoneButtonEnabledByPreference() { + return isVoiceInputEnabledByPreference; } @VisibleForTesting @@ -841,6 +1124,12 @@ public int getKeyboardHeightRatio() { return keyboardHeightRatio; } + @VisibleForTesting + @Override + public HardwareKeyMap getHardwareKeyMap() { + return hardwareKeyboard.getHardwareKeyMap(); + } + @Override public void trimMemory() { if (mozcView != null) { @@ -852,4 +1141,31 @@ public void trimMemory() { public KeyboardActionListener getKeyboardActionListener() { return keyboardActionListener; } + + @Override + public void setCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + if (mozcView != null) { + mozcView.setCursorAnchorInfo(cursorAnchorInfo); + } + } + + @Override + public void setCursorAnchorInfoEnabled(boolean enabled) { + this.cursorAnchroInfoEnabled = enabled; + if (mozcView != null) { + mozcView.setCursorAnchorInfoEnabled(enabled); + } + } + + @Override + public void onShowSymbolInputView() { + isSymbolInputViewVisible = true; + mozcView.resetKeyboardViewState(); + } + + @Override + public void onCloseSymbolInputView() { + isSymbolInputViewVisible = false; + isSymbolInputViewShownByEmojiKey = false; + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java b/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java index bac79187f..efc12b4f1 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java @@ -29,26 +29,27 @@ package org.mozc.android.inputmethod.japanese; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; +import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener; import org.mozc.android.inputmethod.japanese.model.JapaneseSoftwareKeyboardModel; +import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.InputStyle; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.content.res.Configuration; -import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.Window; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; /** @@ -98,6 +99,8 @@ public enum LayoutAdjustment { */ public void consumeKeyOnViewSynchronously(KeyEvent event); + public void onHardwareKeyEvent(KeyEvent keyEvent); + /** * @return whether the view should consume the generic motion event or not. */ @@ -113,7 +116,7 @@ public enum LayoutAdjustment { /** * @return the current keyboard specification. */ - public KeyboardSpecification getJapaneseKeyboardSpecification(); + public KeyboardSpecification getKeyboardSpecification(); /** * Set {@code EditorInfo} instance to the current view. @@ -163,18 +166,28 @@ public enum LayoutAdjustment { public boolean isNarrowMode(); + public boolean isFloatingCandidateMode(); + public void setPopupEnabled(boolean popupEnabled); - public void setHardwareKeyboardCompositionMode(CompositionMode compositionMode); + public void switchHardwareKeyboardCompositionMode(CompositionSwitchMode mode); + + public void setHardwareKeyMap(HardwareKeyMap hardwareKeyMap); + + public void setSkin(Skin skin); - public void setSkinType(SkinType skinType); + public void setMicrophoneButtonEnabledByPreference(boolean microphoneButtonEnabled); - public void setLayoutAdjustment(Resources resources, LayoutAdjustment layoutAdjustment); + public void setLayoutAdjustment(LayoutAdjustment layoutAdjustment); public void setKeyboardHeightRatio(int keyboardHeightRatio); public void onConfigurationChanged(Configuration newConfig); + public void setCursorAnchorInfo(CursorAnchorInfo info); + + public void setCursorAnchorInfoEnabled(boolean enabled); + /** * Reset the status of the current input view. */ @@ -183,11 +196,14 @@ public enum LayoutAdjustment { public void computeInsets( Context context, InputMethodService.Insets outInsets, Window window); + public void onShowSymbolInputView(); + public void onCloseSymbolInputView(); + @VisibleForTesting public ViewEventListener getEventListener(); @VisibleForTesting - public JapaneseSoftwareKeyboardModel getJapaneseSoftwareKeyboardModel(); + public JapaneseSoftwareKeyboardModel getActiveSoftwareKeyboardModel(); @VisibleForTesting public boolean isPopupEnabled(); @@ -199,7 +215,10 @@ public void computeInsets( public EmojiProviderType getEmojiProviderType(); @VisibleForTesting - public SkinType getSkinType(); + public Skin getSkin(); + + @VisibleForTesting + public boolean isMicrophoneButtonEnabledByPreference(); @VisibleForTesting public LayoutAdjustment getLayoutAdjustment(); @@ -207,9 +226,16 @@ public void computeInsets( @VisibleForTesting public int getKeyboardHeightRatio(); + @VisibleForTesting + public HardwareKeyMap getHardwareKeyMap(); + /** * Used for testing to inject key events. */ @VisibleForTesting public KeyboardActionListener getKeyboardActionListener(); + + void updateGlobeButtonEnabled(); + + void updateMicrophoneButtonEnabled(); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java b/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java index cad8979d6..c4463fc9f 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java +++ b/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java @@ -38,6 +38,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Rect; import android.os.Bundle; @@ -113,6 +114,7 @@ private Optional getRow(int virtualViewId) { return Optional.fromNullable(virtualViewIdToRow.get().get(virtualViewId)); } + @SuppressLint("InlinedApi") private Optional createNodeInfoForId(int virtualViewId) { Optional optionalRow = getRow(virtualViewId); if (!optionalRow.isPresent()) { @@ -209,6 +211,9 @@ private void resetVirtualStructure() { * @param y vertical location in screen coordinate (pixel) */ Optional getCandidateWord(int x, int y) { + if (!layout.isPresent()) { + return Optional.absent(); + } for (Row row : layout.get().getRowList()) { if (y < row.getTop() || y >= row.getTop() + row.getHeight()) { continue; diff --git a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java index 5d174d3b2..e4949a2fa 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java +++ b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java @@ -32,11 +32,9 @@ import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction; import org.mozc.android.inputmethod.japanese.keyboard.Key; import org.mozc.android.inputmethod.japanese.keyboard.KeyEntity; -import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler; import org.mozc.android.inputmethod.japanese.keyboard.KeyState; import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; -import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; import org.mozc.android.inputmethod.japanese.resources.R; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; @@ -71,8 +69,6 @@ public class KeyboardAccessibilityDelegate extends AccessibilityDelegateCompat { private final View view; private final KeyboardAccessibilityNodeProvider nodeProvider; private Optional lastHoverKey = Optional.absent(); - // Handler which is called back when key-input should be simulated. - private Optional keyEventHandler = Optional.absent(); // Handler for long-press callback. // Contains 0 or 1 delayed message. private final Handler handler; @@ -84,6 +80,15 @@ public class KeyboardAccessibilityDelegate extends AccessibilityDelegateCompat { // In such case touch-up shouldn't send any key events. // Reset to false when new touch sequence is started. private boolean consumedByLongpress = false; + private final TouchEventEmulator emulator; + + /** + * Emulator interface for touch events (Key input and long press). + */ + public interface TouchEventEmulator { + public void emulateKeyInput(Key key); + public void emulateLongPress(Key key); + } private class LongTapHandler implements Handler.Callback { @Override @@ -96,20 +101,23 @@ public boolean handleMessage(Message msg) { } - public KeyboardAccessibilityDelegate(View view) { + public KeyboardAccessibilityDelegate(View view, TouchEventEmulator emulator) { this(view, new KeyboardAccessibilityNodeProvider(view), view.getContext().getResources().getInteger( - R.integer.config_long_press_key_delay_accessibility)); + R.integer.config_long_press_key_delay_accessibility), + emulator); } @VisibleForTesting KeyboardAccessibilityDelegate(View view, KeyboardAccessibilityNodeProvider nodeProvider, - int longpressDelay) { + int longpressDelay, + TouchEventEmulator emulator) { this.view = Preconditions.checkNotNull(view); this.nodeProvider = Preconditions.checkNotNull(nodeProvider); this.handler = new Handler(new LongTapHandler()); this.longpressDelay = longpressDelay; + this.emulator = Preconditions.checkNotNull(emulator); } private Context getContext() { @@ -210,13 +218,11 @@ private void simulateKeyInput(Key key) { if (!keyState.isPresent()) { return; } - int keyCode = keyState.get().getFlick(Direction.CENTER).getKeyEntity().getKeyCode(); - if (keyCode == KeyEntity.INVALID_KEY_CODE - || !keyEventHandler.isPresent() - || consumedByLongpress) { + int keyCode = keyState.get().getFlick(Direction.CENTER).get().getKeyEntity().getKeyCode(); + if (keyCode == KeyEntity.INVALID_KEY_CODE || consumedByLongpress) { return; } - keyEventHandler.get().sendKey(keyCode, Collections.emptyList()); + emulator.emulateKeyInput(key); } private void simulateLongPress(Key key) { @@ -225,14 +231,12 @@ private void simulateLongPress(Key key) { if (!keyState.isPresent()) { return; } - int longPressKeyCode = keyState.get().getFlick(Direction.CENTER) - .getKeyEntity().getLongPressKeyCode(); - if (longPressKeyCode == KeyEntity.INVALID_KEY_CODE - || !keyEventHandler.isPresent() - || consumedByLongpress) { + int longPressKeyCode = + keyState.get().getFlick(Direction.CENTER).get().getKeyEntity().getLongPressKeyCode(); + if (longPressKeyCode == KeyEntity.INVALID_KEY_CODE || consumedByLongpress) { return; } - keyEventHandler.get().sendKey(longPressKeyCode, Collections.emptyList()); + emulator.emulateLongPress(key); consumedByLongpress = true; } @@ -355,8 +359,4 @@ private boolean shouldObscureInput(boolean isPasswordField) { // Don't speak if the IME is connected to a password field. return isPasswordField; } - - public void setKeyEventHandler(Optional keyEventHandler) { - this.keyEventHandler = Preconditions.checkNotNull(keyEventHandler); - } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java index 570e598c2..ab698dda9 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java +++ b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java @@ -223,7 +223,8 @@ private int getSourceId(Key key) { if (key.isSpacer()) { return UNDEFINED; } - return getKeyState(key, metaState).getFlick(Direction.CENTER).getKeyEntity().getSourceId(); + return getKeyState(key, metaState).getFlick( + Direction.CENTER).get().getKeyEntity().getSourceId(); } private Optional getKeyCode(Key key) { @@ -232,7 +233,7 @@ private Optional getKeyCode(Key key) { return Optional.absent(); } return Optional.of(getKeyState(key, metaState).getFlick( - Direction.CENTER).getKeyEntity().getKeyCode()); + Direction.CENTER).get().getKeyEntity().getKeyCode()); } /** diff --git a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java index 4fb8e5a7c..f0146f206 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java +++ b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java @@ -29,9 +29,9 @@ package org.mozc.android.inputmethod.japanese.hardwarekeyboard; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.MozcLog; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; diff --git a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java index 11e400c01..7b4b23a99 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java +++ b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java @@ -29,11 +29,11 @@ package org.mozc.android.inputmethod.japanese.hardwarekeyboard; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.MozcLog; import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode; import org.mozc.android.inputmethod.japanese.hardwarekeyboard.KeyEventMapperFactory.KeyEventMapper; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap; import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; @@ -98,15 +98,7 @@ public enum HardwareKeyboardSpecification { JAPANESE109A(HardwareKeyMap.JAPANESE109A, KeyEventMapperFactory.JAPANESE_KEYBOARD_MAPPER, KeyboardSpecification.HARDWARE_QWERTY_KANA, - KeyboardSpecification.HARDWARE_QWERTY_ALPHABET), - - /** - * Represents Japanese Mobile "12" Keyboard - */ - TWELVEKEY(HardwareKeyMap.TWELVEKEY, - KeyEventMapperFactory.TWELVEKEY_KEYBOARD_MAPPER, - KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA, - KeyboardSpecification.TWELVE_KEY_TOGGLE_ALPHABET); + KeyboardSpecification.HARDWARE_QWERTY_ALPHABET); /** * Returns true if the given {@code codepoint} is printable. @@ -214,10 +206,8 @@ public static void maybeSetDetectedHardwareKeyMap( HardwareKeyMap detectedKeyMap = null; switch(configuration.keyboard) { case Configuration.KEYBOARD_12KEY: - detectedKeyMap = HardwareKeyMap.TWELVEKEY; - break; case Configuration.KEYBOARD_QWERTY: - detectedKeyMap = HardwareKeyMap.JAPANESE109A; + detectedKeyMap = HardwareKeyMap.DEFAULT; break; case Configuration.KEYBOARD_NOKEYS: case Configuration.KEYBOARD_UNDEFINED: @@ -477,8 +467,8 @@ public int getKeyCode() { } @Override - public android.view.KeyEvent getNativeEvent() { - return keyEvent; + public Optional getNativeEvent() { + return Optional.of(keyEvent); } }; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java index 57bd6f58c..71768d8da 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java @@ -53,7 +53,6 @@ public interface KeyEventMapper { } public static final KeyEventMapper DEFAULT_KEYBOARD_MAPPER = new DefaultKeyboardMapper(); - public static final KeyEventMapper TWELVEKEY_KEYBOARD_MAPPER = new TwelvekeyKeyboardMapper(); public static final KeyEventMapper JAPANESE_KEYBOARD_MAPPER = new JapaneseKeyboardMapper(); /** @@ -68,25 +67,6 @@ public void applyMapping(CompactKeyEvent keyEvent) { } } - /** - * Similar to DefaultKeyboardMapper but additional conversion for POUND and CENTER keys. - */ - private static class TwelvekeyKeyboardMapper implements KeyEventMapper { - static void doKeyCharacterMapping(CompactKeyEvent keyEvent) { - int originalKeyCode = keyEvent.getKeyCode(); - if (originalKeyCode == KeyEvent.KEYCODE_POUND - || originalKeyCode == KeyEvent.KEYCODE_DPAD_CENTER) { - keyEvent.setKeyCode(KeyEvent.KEYCODE_ENTER); - } - } - @Override - public void applyMapping(CompactKeyEvent keyEvent) { - keyEvent.setKeyCode(doKeyLayoutMappingForOldAndroids(keyEvent.getKeyCode(), - keyEvent.getScanCode())); - doKeyCharacterMapping(keyEvent); - } - } - /** * Key-layout and key-character mapping for Japanese keyboard. */ diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java index 9414ce6ce..90741385a 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java @@ -29,19 +29,25 @@ package org.mozc.android.inputmethod.japanese.keyboard; +import org.mozc.android.inputmethod.japanese.resources.R; import org.mozc.android.inputmethod.japanese.view.CandidateBackgroundDrawable; import org.mozc.android.inputmethod.japanese.view.CandidateBackgroundFocusedDrawable; import org.mozc.android.inputmethod.japanese.view.CenterCircularHighlightDrawable; -import org.mozc.android.inputmethod.japanese.view.LightIconDrawable; -import org.mozc.android.inputmethod.japanese.view.PopUpFrameWindowDrawable; +import org.mozc.android.inputmethod.japanese.view.DummyDrawable; +import org.mozc.android.inputmethod.japanese.view.QwertySpaceKeyDrawable; import org.mozc.android.inputmethod.japanese.view.RectKeyDrawable; import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; import org.mozc.android.inputmethod.japanese.view.ThreeDotsIconDrawable; import org.mozc.android.inputmethod.japanese.view.TriangularHighlightDrawable; import org.mozc.android.inputmethod.japanese.view.TriangularHighlightDrawable.HighlightDirection; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import android.content.res.Resources; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.StateListDrawable; @@ -83,8 +89,14 @@ public enum DrawableType { QWERTY_REGULAR_KEY_BACKGROUND, QWERTY_FUNCTION_KEY_BACKGROUND, QWERTY_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS, - QWERTY_FUNCTION_KEY_LIGHT_ON_BACKGROUND, - QWERTY_FUNCTION_KEY_LIGHT_OFF_BACKGROUND, + QWERTY_FUNCTION_KEY_SPACE_WITH_THREEDOTS, + + // Separator on keyboard. + KEYBOARD_SEPARATOR_TOP, + KEYBOARD_SEPARATOR_CENTER, + KEYBOARD_SEPARATOR_BOTTOM, + + TRNASPARENT, // Highlight for flicking. TWELVEKEYS_CENTER_FLICK, @@ -104,366 +116,327 @@ public enum DrawableType { // According to the original design mock, the pressed key background contains some padding. // Here are hardcoded parameters. - private static final float TWELVEKEYS_LEFT_OFFSET = 0.5f; - private static final float TWELVEKEYS_TOP_OFFSET = 0; - private static final float TWELVEKEYS_RIGHT_OFFSET = 1.0f; - private static final float TWELVEKEYS_BOTTOM_OFFSET = 1.0f; + public static final float POPUP_WINDOW_PADDING = 7.0f; + private static final float TWELVEKEYS_THREEDOTS_BOTTOM_OFFSET = 3.0f; private static final float TWELVEKEYS_THREEDOTS_RIGHT_OFFSET = 5.0f; private static final float TWELVEKEYS_THREEDOTS_WIDTH = 2.0f; private static final float TWELVEKEYS_THREEDOTS_SPAN = 2.0f; - private static final float QWERTY_LEFT_OFFSET = 2.0f; - private static final float QWERTY_TOP_OFFSET = 1.0f; - private static final float QWERTY_RIGHT_OFFSET = 2.0f; - private static final float QWERTY_BOTTOM_OFFSET = 3.0f; private static final float QWERTY_THREEDOTS_BOTTOM_OFFSET = 3.0f; private static final float QWERTY_THREEDOTS_RIGHT_OFFSET = 5.0f; private static final float QWERTY_THREEDOTS_WIDTH = 2.0f; private static final float QWERTY_THREEDOTS_SPAN = 2.0f; - private static final float QWERTY_LIGHT_TOP_OFFSET = 2.0f; - private static final float QWERTY_LIGHT_RIGHT_OFFSET = 2.0f; - private static final float QWERTY_LIGHT_RADIUS = 2.25f; - - private static final float POPUP_WINDOW_PADDING = 7.0f; - private static final float POPUP_FRAME_BORDER_WIDTH = 5.0f; - private static final float POPUP_SHADOW_SIZE = 1.5f; private static final float CANDIDATE_BACKGROUND_PADDING = 0.0f; private static final float SYMBOL_CANDIDATE_BACKGROUND_PADDING = 0.0f; + private final Resources resources; private final float density; private final Map drawableMap = new EnumMap(DrawableType.class); - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; + private Skin skin = Skin.getFallbackInstance(); - public BackgroundDrawableFactory(float density) { - this.density = density; + public BackgroundDrawableFactory(Resources resources) { + this.resources = Preconditions.checkNotNull(resources); + density = resources.getDisplayMetrics().density; } public Drawable getDrawable(DrawableType drawableType) { - if (drawableType == null) { - return null; - } - - Drawable result = drawableMap.get(drawableType); + Drawable result = drawableMap.get(Preconditions.checkNotNull(drawableType)); if (result == null) { result = createBackgroundDrawable(drawableType); drawableMap.put(drawableType, result); } - return result; } - public void setSkinType(SkinType skinType) { - if (this.skinType == skinType) { + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + if (this.skin.equals(skin)) { return; } - this.skinType = skinType; + this.skin = skin; drawableMap.clear(); } private Drawable createBackgroundDrawable(DrawableType drawableType) { - switch (drawableType) { + switch (Preconditions.checkNotNull(drawableType)) { case TWELVEKEYS_REGULAR_KEY_BACKGROUND: return createPressableDrawable( new RectKeyDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.twelvekeysLayoutPressedKeyTopColor, - skinType.twelvekeysLayoutPressedKeyBottomColor, - skinType.twelvekeysLayoutPressedKeyHighlightColor, - skinType.twelvekeysLayoutPressedKeyLightShadeColor, - skinType.twelvekeysLayoutPressedKeyDarkShadeColor, - skinType.twelvekeysLayoutPressedKeyShadowColor), - new RectKeyDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.twelvekeysLayoutReleasedKeyTopColor, - skinType.twelvekeysLayoutReleasedKeyBottomColor, - skinType.twelvekeysLayoutReleasedKeyHighlightColor, - skinType.twelvekeysLayoutReleasedKeyLightShadeColor, - skinType.twelvekeysLayoutReleasedKeyDarkShadeColor, - skinType.twelvekeysLayoutReleasedKeyShadowColor)); + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.twelvekeysLayoutPressedKeyTopColor, + skin.twelvekeysLayoutPressedKeyBottomColor, + skin.twelvekeysLayoutPressedKeyHighlightColor, + skin.twelvekeysLayoutPressedKeyLightShadeColor, + skin.twelvekeysLayoutPressedKeyDarkShadeColor, + skin.twelvekeysLayoutPressedKeyShadowColor), + Optional.of(new RectKeyDrawable( + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.twelvekeysLayoutReleasedKeyTopColor, + skin.twelvekeysLayoutReleasedKeyBottomColor, + skin.twelvekeysLayoutReleasedKeyHighlightColor, + skin.twelvekeysLayoutReleasedKeyLightShadeColor, + skin.twelvekeysLayoutReleasedKeyDarkShadeColor, + skin.twelvekeysLayoutReleasedKeyShadowColor))); case TWELVEKEYS_FUNCTION_KEY_BACKGROUND: return createPressableDrawable( new RectKeyDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.twelvekeysLayoutPressedFunctionKeyTopColor, - skinType.twelvekeysLayoutPressedFunctionKeyBottomColor, - skinType.twelvekeysLayoutPressedFunctionKeyHighlightColor, - skinType.twelvekeysLayoutPressedFunctionKeyLightShadeColor, - skinType.twelvekeysLayoutPressedFunctionKeyDarkShadeColor, - skinType.twelvekeysLayoutPressedFunctionKeyShadowColor), - new RectKeyDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.twelvekeysLayoutReleasedFunctionKeyTopColor, - skinType.twelvekeysLayoutReleasedFunctionKeyBottomColor, - skinType.twelvekeysLayoutReleasedFunctionKeyHighlightColor, - skinType.twelvekeysLayoutReleasedFunctionKeyLightShadeColor, - skinType.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor, - skinType.twelvekeysLayoutReleasedFunctionKeyShadowColor)); + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.twelvekeysLayoutPressedFunctionKeyTopColor, + skin.twelvekeysLayoutPressedFunctionKeyBottomColor, + skin.twelvekeysLayoutPressedFunctionKeyHighlightColor, + skin.twelvekeysLayoutPressedFunctionKeyLightShadeColor, + skin.twelvekeysLayoutPressedFunctionKeyDarkShadeColor, + skin.twelvekeysLayoutPressedFunctionKeyShadowColor), + Optional.of(new RectKeyDrawable( + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.twelvekeysLayoutReleasedFunctionKeyTopColor, + skin.twelvekeysLayoutReleasedFunctionKeyBottomColor, + skin.twelvekeysLayoutReleasedFunctionKeyHighlightColor, + skin.twelvekeysLayoutReleasedFunctionKeyLightShadeColor, + skin.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor, + skin.twelvekeysLayoutReleasedFunctionKeyShadowColor))); case TWELVEKEYS_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS: return new LayerDrawable(new Drawable[] { createPressableDrawable( new RectKeyDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.twelvekeysLayoutPressedFunctionKeyTopColor, - skinType.twelvekeysLayoutPressedFunctionKeyBottomColor, - skinType.twelvekeysLayoutPressedFunctionKeyHighlightColor, - skinType.twelvekeysLayoutPressedFunctionKeyLightShadeColor, - skinType.twelvekeysLayoutPressedFunctionKeyDarkShadeColor, - skinType.twelvekeysLayoutPressedFunctionKeyShadowColor), - new RectKeyDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.twelvekeysLayoutReleasedFunctionKeyTopColor, - skinType.twelvekeysLayoutReleasedFunctionKeyBottomColor, - skinType.twelvekeysLayoutReleasedFunctionKeyHighlightColor, - skinType.twelvekeysLayoutReleasedFunctionKeyLightShadeColor, - skinType.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor, - skinType.twelvekeysLayoutReleasedFunctionKeyShadowColor)), + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.twelvekeysLayoutPressedFunctionKeyTopColor, + skin.twelvekeysLayoutPressedFunctionKeyBottomColor, + skin.twelvekeysLayoutPressedFunctionKeyHighlightColor, + skin.twelvekeysLayoutPressedFunctionKeyLightShadeColor, + skin.twelvekeysLayoutPressedFunctionKeyDarkShadeColor, + skin.twelvekeysLayoutPressedFunctionKeyShadowColor), + Optional.of(new RectKeyDrawable( + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.twelvekeysLayoutReleasedFunctionKeyTopColor, + skin.twelvekeysLayoutReleasedFunctionKeyBottomColor, + skin.twelvekeysLayoutReleasedFunctionKeyHighlightColor, + skin.twelvekeysLayoutReleasedFunctionKeyLightShadeColor, + skin.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor, + skin.twelvekeysLayoutReleasedFunctionKeyShadowColor))), new ThreeDotsIconDrawable( - (int) ((TWELVEKEYS_BOTTOM_OFFSET + TWELVEKEYS_THREEDOTS_BOTTOM_OFFSET) * density), - (int) ((TWELVEKEYS_RIGHT_OFFSET + TWELVEKEYS_THREEDOTS_RIGHT_OFFSET) * density), - skinType.threeDotsColor, + (int) (skin.twelvekeysBottomOffsetDimension + + TWELVEKEYS_THREEDOTS_BOTTOM_OFFSET * density), + (int) (skin.twelvekeysRightOffsetDimension + + TWELVEKEYS_THREEDOTS_RIGHT_OFFSET * density), + skin.threeDotsColor, (int) (TWELVEKEYS_THREEDOTS_WIDTH * density), (int) (TWELVEKEYS_THREEDOTS_SPAN * density))}); case QWERTY_REGULAR_KEY_BACKGROUND: return createPressableDrawable( new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutPressedKeyTopColor, - skinType.qwertyLayoutPressedKeyBottomColor, - skinType.qwertyLayoutPressedKeyHighlightColor, - skinType.qwertyLayoutPressedKeyShadowColor), - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutReleasedKeyTopColor, - skinType.qwertyLayoutReleasedKeyBottomColor, - skinType.qwertyLayoutReleasedKeyHighlightColor, - skinType.qwertyLayoutReleasedKeyShadowColor)); + (int) (skin.qwertyLeftOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertyRightOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertyRoundRadiusDimension), + skin.qwertyLayoutPressedKeyTopColor, + skin.qwertyLayoutPressedKeyBottomColor, + skin.qwertyLayoutPressedKeyHighlightColor, + skin.qwertyLayoutPressedKeyShadowColor), + Optional.of(new RoundRectKeyDrawable( + (int) (skin.qwertyLeftOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertyRightOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertyRoundRadiusDimension), + skin.qwertyLayoutReleasedKeyTopColor, + skin.qwertyLayoutReleasedKeyBottomColor, + skin.qwertyLayoutReleasedKeyHighlightColor, + skin.qwertyLayoutReleasedKeyShadowColor))); case QWERTY_FUNCTION_KEY_BACKGROUND: return createPressableDrawable( new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutPressedFunctionKeyTopColor, - skinType.qwertyLayoutPressedFunctionKeyBottomColor, - skinType.qwertyLayoutPressedFunctionKeyHighlightColor, - skinType.qwertyLayoutPressedFunctionKeyShadowColor), - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutReleasedFunctionKeyTopColor, - skinType.qwertyLayoutReleasedFunctionKeyBottomColor, - skinType.qwertyLayoutReleasedFunctionKeyHighlightColor, - skinType.qwertyLayoutReleasedFunctionKeyShadowColor)); + (int) (skin.qwertyLeftOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertyRightOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertyRoundRadiusDimension), + skin.qwertyLayoutPressedFunctionKeyTopColor, + skin.qwertyLayoutPressedFunctionKeyBottomColor, + skin.qwertyLayoutPressedFunctionKeyHighlightColor, + skin.qwertyLayoutPressedFunctionKeyShadowColor), + Optional.of(new RoundRectKeyDrawable( + (int) (skin.qwertyLeftOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertyRightOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertyRoundRadiusDimension), + skin.qwertyLayoutReleasedFunctionKeyTopColor, + skin.qwertyLayoutReleasedFunctionKeyBottomColor, + skin.qwertyLayoutReleasedFunctionKeyHighlightColor, + skin.qwertyLayoutReleasedFunctionKeyShadowColor))); case QWERTY_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS: return new LayerDrawable(new Drawable[] { createPressableDrawable( new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutPressedFunctionKeyTopColor, - skinType.qwertyLayoutPressedFunctionKeyBottomColor, - skinType.qwertyLayoutPressedFunctionKeyHighlightColor, - skinType.qwertyLayoutPressedFunctionKeyShadowColor), - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutReleasedFunctionKeyTopColor, - skinType.qwertyLayoutReleasedFunctionKeyBottomColor, - skinType.qwertyLayoutReleasedFunctionKeyHighlightColor, - skinType.qwertyLayoutReleasedFunctionKeyShadowColor)), + (int) (skin.qwertyLeftOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertyRightOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertyRoundRadiusDimension), + skin.qwertyLayoutPressedFunctionKeyTopColor, + skin.qwertyLayoutPressedFunctionKeyBottomColor, + skin.qwertyLayoutPressedFunctionKeyHighlightColor, + skin.qwertyLayoutPressedFunctionKeyShadowColor), + Optional.of(new RoundRectKeyDrawable( + (int) (skin.qwertyLeftOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertyRightOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertyRoundRadiusDimension), + skin.qwertyLayoutReleasedFunctionKeyTopColor, + skin.qwertyLayoutReleasedFunctionKeyBottomColor, + skin.qwertyLayoutReleasedFunctionKeyHighlightColor, + skin.qwertyLayoutReleasedFunctionKeyShadowColor))), new ThreeDotsIconDrawable( - (int) ((QWERTY_BOTTOM_OFFSET + QWERTY_THREEDOTS_BOTTOM_OFFSET) * density), - (int) ((QWERTY_RIGHT_OFFSET + QWERTY_THREEDOTS_RIGHT_OFFSET) * density), - skinType.threeDotsColor, + (int) (skin.qwertyBottomOffsetDimension + + (QWERTY_THREEDOTS_BOTTOM_OFFSET * density)), + (int) (skin.qwertyRightOffsetDimension + + (QWERTY_THREEDOTS_RIGHT_OFFSET * density)), + skin.threeDotsColor, (int) (QWERTY_THREEDOTS_WIDTH * density), (int) (QWERTY_THREEDOTS_SPAN * density))}); - case QWERTY_FUNCTION_KEY_LIGHT_ON_BACKGROUND: + case QWERTY_FUNCTION_KEY_SPACE_WITH_THREEDOTS: return new LayerDrawable(new Drawable[] { createPressableDrawable( - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutPressedFunctionKeyTopColor, - skinType.qwertyLayoutPressedFunctionKeyBottomColor, - skinType.qwertyLayoutPressedFunctionKeyHighlightColor, - skinType.qwertyLayoutPressedFunctionKeyShadowColor), - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutReleasedFunctionKeyTopColor, - skinType.qwertyLayoutReleasedFunctionKeyBottomColor, - skinType.qwertyLayoutReleasedFunctionKeyHighlightColor, - skinType.qwertyLayoutReleasedFunctionKeyShadowColor)), - new LightIconDrawable( - (int) ((QWERTY_TOP_OFFSET + QWERTY_LIGHT_TOP_OFFSET + QWERTY_LIGHT_RADIUS) - * density), - (int) ((QWERTY_RIGHT_OFFSET + QWERTY_LIGHT_RIGHT_OFFSET + QWERTY_LIGHT_RADIUS) - * density), - skinType.qwertyLightOnSignLightColor, - skinType.qwertyLightOnSignDarkColor, - skinType.qwertyLightOnSignShadeColor, - (int) (QWERTY_LIGHT_RADIUS * density)) - }); - - case QWERTY_FUNCTION_KEY_LIGHT_OFF_BACKGROUND: - return new LayerDrawable(new Drawable[] { - createPressableDrawable( - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutPressedFunctionKeyTopColor, - skinType.qwertyLayoutPressedFunctionKeyBottomColor, - skinType.qwertyLayoutPressedFunctionKeyHighlightColor, - skinType.qwertyLayoutPressedFunctionKeyShadowColor), - new RoundRectKeyDrawable( - (int) (QWERTY_LEFT_OFFSET * density), - (int) (QWERTY_TOP_OFFSET * density), - (int) (QWERTY_RIGHT_OFFSET * density), - (int) (QWERTY_BOTTOM_OFFSET * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.qwertyLayoutReleasedFunctionKeyTopColor, - skinType.qwertyLayoutReleasedFunctionKeyBottomColor, - skinType.qwertyLayoutReleasedFunctionKeyHighlightColor, - skinType.qwertyLayoutReleasedFunctionKeyShadowColor)), - new LightIconDrawable( - (int) ((QWERTY_TOP_OFFSET + QWERTY_LIGHT_TOP_OFFSET + QWERTY_LIGHT_RADIUS) - * density), - (int) ((QWERTY_RIGHT_OFFSET + QWERTY_LIGHT_RIGHT_OFFSET + QWERTY_LIGHT_RADIUS) - * density), - skinType.qwertyLightOffSignLightColor, - skinType.qwertyLightOffSignDarkColor, - skinType.qwertyLightOffSignShadeColor, - (int) (QWERTY_LIGHT_RADIUS * density)) - }); + new QwertySpaceKeyDrawable( + (int) (skin.qwertySpaceKeyHeightDimension), + (int) (skin.qwertySpaceKeyHorizontalOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertySpaceKeyHorizontalOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertySpaceKeyRoundRadiusDimension), + skin.qwertyLayoutPressedSpaceKeyTopColor, + skin.qwertyLayoutPressedSpaceKeyBottomColor, + skin.qwertyLayoutPressedSpaceKeyHighlightColor, + skin.qwertyLayoutPressedSpaceKeyShadowColor), + Optional.of(new QwertySpaceKeyDrawable( + (int) (skin.qwertySpaceKeyHeightDimension), + (int) (skin.qwertySpaceKeyHorizontalOffsetDimension), + (int) (skin.qwertyTopOffsetDimension), + (int) (skin.qwertySpaceKeyHorizontalOffsetDimension), + (int) (skin.qwertyBottomOffsetDimension), + (int) (skin.qwertySpaceKeyRoundRadiusDimension), + skin.qwertyLayoutReleasedSpaceKeyTopColor, + skin.qwertyLayoutReleasedSpaceKeyBottomColor, + skin.qwertyLayoutReleasedSpaceKeyHighlightColor, + skin.qwertyLayoutReleasedSpaceKeyShadowColor))), + new ThreeDotsIconDrawable( + (int) (skin.qwertyBottomOffsetDimension + + (QWERTY_THREEDOTS_BOTTOM_OFFSET * density)), + (int) (skin.qwertySpaceKeyHorizontalOffsetDimension + + (QWERTY_THREEDOTS_RIGHT_OFFSET * density)), + skin.threeDotsColor, + (int) (QWERTY_THREEDOTS_WIDTH * density), + (int) (QWERTY_THREEDOTS_SPAN * density))}); + + case KEYBOARD_SEPARATOR_TOP: + return new InsetDrawable( + new ColorDrawable(skin.keyboardSeparatorColor), 0, + resources.getDimensionPixelSize(R.dimen.keyboard_separator_padding), + 0, 0); + + case KEYBOARD_SEPARATOR_CENTER: + return new ColorDrawable(skin.keyboardSeparatorColor); + + case KEYBOARD_SEPARATOR_BOTTOM: + return new InsetDrawable( + new ColorDrawable(skin.keyboardSeparatorColor), 0, 0, 0, + resources.getDimensionPixelSize(R.dimen.keyboard_separator_padding)); + + case TRNASPARENT: + return DummyDrawable.getInstance(); case TWELVEKEYS_CENTER_FLICK: return new CenterCircularHighlightDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.flickBaseColor, - skinType.flickShadeColor); + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.flickBaseColor, + skin.flickShadeColor); case TWELVEKEYS_LEFT_FLICK: return new TriangularHighlightDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.flickBaseColor, - skinType.flickShadeColor, + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.flickBaseColor, + skin.flickShadeColor, HighlightDirection.LEFT); case TWELVEKEYS_UP_FLICK: return new TriangularHighlightDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.flickBaseColor, - skinType.flickShadeColor, + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.flickBaseColor, + skin.flickShadeColor, HighlightDirection.UP); case TWELVEKEYS_RIGHT_FLICK: return new TriangularHighlightDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.flickBaseColor, - skinType.flickShadeColor, + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.flickBaseColor, + skin.flickShadeColor, HighlightDirection.RIGHT); case TWELVEKEYS_DOWN_FLICK: return new TriangularHighlightDrawable( - (int) (TWELVEKEYS_LEFT_OFFSET * density), - (int) (TWELVEKEYS_TOP_OFFSET * density), - (int) (TWELVEKEYS_RIGHT_OFFSET * density), - (int) (TWELVEKEYS_BOTTOM_OFFSET * density), - skinType.flickBaseColor, - skinType.flickShadeColor, + (int) (skin.twelvekeysLeftOffsetDimension), + (int) (skin.twelvekeysTopOffsetDimension), + (int) (skin.twelvekeysRightOffsetDimension), + (int) (skin.twelvekeysBottomOffsetDimension), + skin.flickBaseColor, + skin.flickShadeColor, HighlightDirection.DOWN); case POPUP_BACKGROUND_WINDOW: - // TODO(hidehiko): add a flag to control popup style in SkinType. - return skinType == SkinType.ORANGE_LIGHTGRAY - ? new PopUpFrameWindowDrawable( - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_FRAME_BORDER_WIDTH * density), - POPUP_SHADOW_SIZE * density, - skinType.popupFrameWindowTopColor, - skinType.popupFrameWindowBottomColor, - skinType.popupFrameWindowBorderColor, - skinType.popupFrameWindowInnerPaneColor) - : new RoundRectKeyDrawable( - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_WINDOW_PADDING * density), - (int) (POPUP_WINDOW_PADDING * density), - (int) (skinType.qwertyKeyRoundRadius * density), - skinType.popupFrameWindowTopColor, - skinType.popupFrameWindowBottomColor, - 0, // No highlight. - skinType.popupFrameWindowShadowColor); + return + new RoundRectKeyDrawable( + (int) (POPUP_WINDOW_PADDING * density), + (int) (POPUP_WINDOW_PADDING * density), + (int) (POPUP_WINDOW_PADDING * density), + (int) (POPUP_WINDOW_PADDING * density), + (int) (skin.qwertyRoundRadiusDimension), + skin.popupFrameWindowTopColor, + skin.popupFrameWindowBottomColor, + 0, // No highlight. + skin.popupFrameWindowShadowColor); + case CANDIDATE_BACKGROUND: return createFocusableDrawable( new CandidateBackgroundFocusedDrawable( @@ -471,18 +444,19 @@ private Drawable createBackgroundDrawable(DrawableType drawableType) { (int) (CANDIDATE_BACKGROUND_PADDING * density), (int) (CANDIDATE_BACKGROUND_PADDING * density), (int) (CANDIDATE_BACKGROUND_PADDING * density), - skinType.candidateBackgroundFocusedTopColor, - skinType.candidateBackgroundFocusedBottomColor, - skinType.candidateBackgroundFocusedShadowColor), - new CandidateBackgroundDrawable( + skin.candidateBackgroundFocusedTopColor, + skin.candidateBackgroundFocusedBottomColor, + skin.candidateBackgroundFocusedShadowColor), + Optional.of(new CandidateBackgroundDrawable( (int) (CANDIDATE_BACKGROUND_PADDING * density), (int) (CANDIDATE_BACKGROUND_PADDING * density), (int) (CANDIDATE_BACKGROUND_PADDING * density), (int) (CANDIDATE_BACKGROUND_PADDING * density), - skinType.candidateBackgroundTopColor, - skinType.candidateBackgroundBottomColor, - skinType.candidateBackgroundHighlightColor, - skinType.candidateBackgroundBorderColor)); + skin.candidateBackgroundTopColor, + skin.candidateBackgroundBottomColor, + skin.candidateBackgroundHighlightColor, + skin.candidateBackgroundBorderColor))); + case SYMBOL_CANDIDATE_BACKGROUND: return createFocusableDrawable( new CandidateBackgroundFocusedDrawable( @@ -490,18 +464,18 @@ private Drawable createBackgroundDrawable(DrawableType drawableType) { (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), - skinType.candidateBackgroundFocusedTopColor, - skinType.candidateBackgroundFocusedBottomColor, - skinType.candidateBackgroundFocusedShadowColor), - new CandidateBackgroundDrawable( + skin.candidateBackgroundFocusedTopColor, + skin.candidateBackgroundFocusedBottomColor, + skin.candidateBackgroundFocusedShadowColor), + Optional.of(new CandidateBackgroundDrawable( (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density), - skinType.symbolCandidateBackgroundTopColor, - skinType.symbolCandidateBackgroundBottomColor, - skinType.symbolCandidateBackgroundHighlightColor, - skinType.symbolCandidateBackgroundBorderColor)); + skin.symbolCandidateBackgroundTopColor, + skin.symbolCandidateBackgroundBottomColor, + skin.symbolCandidateBackgroundHighlightColor, + skin.symbolCandidateBackgroundBorderColor))); } throw new IllegalArgumentException("Unknown drawable type: " + drawableType); @@ -509,29 +483,32 @@ private Drawable createBackgroundDrawable(DrawableType drawableType) { // TODO(hidehiko): Move this method to the somewhere in view package. public static Drawable createPressableDrawable( - Drawable pressedDrawable, Drawable releasedDrawable) { + Drawable pressedDrawable, Optional releasedDrawable) { return createTwoStateDrawable( - android.R.attr.state_pressed, pressedDrawable, releasedDrawable); + android.R.attr.state_pressed, Preconditions.checkNotNull(pressedDrawable), + Preconditions.checkNotNull(releasedDrawable)); } public static Drawable createFocusableDrawable( - Drawable focusedDrawable, Drawable unfocusedDrawable) { + Drawable focusedDrawable, Optional unfocusedDrawable) { return createTwoStateDrawable( - android.R.attr.state_focused, focusedDrawable, unfocusedDrawable); + android.R.attr.state_focused, Preconditions.checkNotNull(focusedDrawable), + Preconditions.checkNotNull(unfocusedDrawable)); } public static Drawable createSelectableDrawable( - Drawable selectedDrawable, Drawable unselectedDrawable) { + Drawable selectedDrawable, Optional unselectedDrawable) { return createTwoStateDrawable( - android.R.attr.state_selected, selectedDrawable, unselectedDrawable); + android.R.attr.state_selected, Preconditions.checkNotNull(selectedDrawable), + Preconditions.checkNotNull(unselectedDrawable)); } private static Drawable createTwoStateDrawable( - int state, Drawable enabledDrawable, Drawable disabledDrawable) { + int state, Drawable enabledDrawable, Optional disabledDrawable) { StateListDrawable drawable = new StateListDrawable(); drawable.addState(new int[] { state }, enabledDrawable); - if (disabledDrawable != null) { - drawable.addState(EMPTY_STATE, disabledDrawable); + if (disabledDrawable.isPresent()) { + drawable.addState(EMPTY_STATE, disabledDrawable.get()); } return drawable; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java index 3ca9dab34..3fe183bf9 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java @@ -30,6 +30,7 @@ package org.mozc.android.inputmethod.japanese.keyboard; import com.google.common.base.Objects; +import com.google.common.base.Preconditions; /** * A class corresponding to {@code <Flick>} element in xml resource files. @@ -61,15 +62,10 @@ public static Direction valueOf(int index) { private final Flick.Direction direction; private final KeyEntity keyEntity; + public Flick(Direction direction, KeyEntity keyEntity) { - if (direction == null) { - throw new NullPointerException("direction shouldn't be null."); - } - if (keyEntity == null) { - throw new NullPointerException("keyEntity shouldn't be null."); - } - this.direction = direction; - this.keyEntity = keyEntity; + this.direction = Preconditions.checkNotNull(direction); + this.keyEntity = Preconditions.checkNotNull(keyEntity); } public Direction getDirection() { diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java index d60590ebc..8e515883d 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java @@ -29,6 +29,7 @@ package org.mozc.android.inputmethod.japanese.keyboard; +import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; import com.google.common.base.Objects; import com.google.common.base.Objects.ToStringHelper; import com.google.common.base.Optional; @@ -57,7 +58,6 @@ *

    • {@code edgeFlags}: flags whether the key should stick to keyboard's boundary. *
    • {@code isRepeatable}: whether the key long press cause a repeated key tapping. *
    • {@code isModifier}: whether the key is modifier (e.g. shift key). - *
    • {@code isSticky}: whether the key behaves sticky (e.g. caps lock). *
    * * The {@code <Key>} element can have (at most) two {@code <KeyState>} elements. @@ -86,16 +86,24 @@ public enum Stick { private final int edgeFlags; private final boolean isRepeatable; private final boolean isModifier; - private final boolean isSticky; private final Stick stick; // Default KeyState. // Absent if this is a spacer. private final Optional defaultKeyState; + private final DrawableType keyBackgroundDrawableType; + private final List keyStateList; public Key(int x, int y, int width, int height, int horizontalGap, - int edgeFlags, boolean isRepeatable, boolean isModifier, boolean isSticky, - Stick stick, List keyStateList) { + int edgeFlags, boolean isRepeatable, boolean isModifier, + Stick stick, DrawableType keyBackgroundDrawableType, + List keyStateList) { + Preconditions.checkNotNull(stick); + Preconditions.checkNotNull(keyBackgroundDrawableType); + Preconditions.checkNotNull(keyStateList); + Preconditions.checkArgument(width >= 0); + Preconditions.checkArgument(height >= 0); + this.x = x; this.y = y; this.width = width; @@ -104,28 +112,29 @@ public Key(int x, int y, int width, int height, int horizontalGap, this.edgeFlags = edgeFlags; this.isRepeatable = isRepeatable; this.isModifier = isModifier; - this.isSticky = isSticky; this.stick = stick; + this.keyBackgroundDrawableType = keyBackgroundDrawableType; List tmpKeyStateList = null; // Lazy creation. Optional defaultKeyState = Optional.absent(); for (KeyState keyState : keyStateList) { Set metaStateSet = keyState.getMetaStateSet(); - if (metaStateSet.isEmpty()) { + if (metaStateSet.isEmpty() || metaStateSet.contains(KeyState.MetaState.FALLBACK)) { if (defaultKeyState.isPresent()) { throw new IllegalArgumentException("Found duplicate default meta state"); } defaultKeyState = Optional.of(keyState); - continue; + if (metaStateSet.size() <= 1) { // metaStateSet contains only FALLBACK + continue; + } } if (tmpKeyStateList == null) { tmpKeyStateList = new ArrayList(); } tmpKeyStateList.add(keyState); } - if (!defaultKeyState.isPresent() && tmpKeyStateList != null) { - throw new IllegalArgumentException("Default KeyState is mandatory for non-spacer."); - } + Preconditions.checkArgument(defaultKeyState.isPresent() || tmpKeyStateList == null, + "Default KeyState is mandatory for non-spacer."); this.defaultKeyState = defaultKeyState; this.keyStateList = tmpKeyStateList == null ? Collections.emptyList() @@ -164,14 +173,14 @@ public boolean isModifier() { return isModifier; } - public boolean isSticky() { - return isSticky; - } - public Stick getStick() { return stick; } + public DrawableType getKeyBackgroundDrawableType() { + return keyBackgroundDrawableType; + } + /** * Returns {@code KeyState} at least one of which the metaState is in given {@code metaStates}. *

    diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java index 9da46294d..5184f09ab 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java @@ -29,8 +29,9 @@ package org.mozc.android.inputmethod.japanese.keyboard; -import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; /** * A class corresponding to a {@code <KeyEntity>} element in xml resource files. @@ -39,45 +40,43 @@ * */ public class KeyEntity { + /** INVALID_KEY_CODE represents the key has no key code to call the Mozc server back. */ public static final int INVALID_KEY_CODE = Integer.MIN_VALUE; private final int sourceId; private final int keyCode; private final int longPressKeyCode; + private final boolean longPressTimeoutTrigger; private final int keyIconResourceId; // If |keyIconResourceId| is empty and |keyCharacter| is set, // use this character for rendering key top. // TODO(team): Implement rendering method. - private final String keyCharacter; - // TODO(hidehiko): Move this field to Key. - private final DrawableType keyBackgroundDrawableType; + private final Optional keyCharacter; private final boolean flickHighlightEnabled; - private final PopUp popUp; - - // Constructor for compatibility. - // TODO(team): Remove this constructor after changing all callers including tests. - public KeyEntity(int sourceId, int keyCode, int longPressKeyCode, - int keyIconResourceId, - DrawableType keyBackgroundDrawableType, - boolean flickHighlightEnabled, PopUp popUp) { - this( - sourceId, keyCode, longPressKeyCode, keyIconResourceId, null, keyBackgroundDrawableType, - flickHighlightEnabled, popUp); - } + private final Optional popUp; + private final int horizontalPadding; + private final int verticalPadding; + private final int iconWidth; + private final int iconHeight; public KeyEntity(int sourceId, int keyCode, int longPressKeyCode, - int keyIconResourceId, String keyCharacter, - DrawableType keyBackgroundDrawableType, - boolean flickHighlightEnabled, PopUp popUp) { + boolean longPressTimeoutTrigger, int keyIconResourceId, + Optional keyCharacter, boolean flickHighlightEnabled, + Optional popUp, int horizontalPadding, int verticalPadding, + int iconWidth, int iconHeight) { this.sourceId = sourceId; this.keyCode = keyCode; this.longPressKeyCode = longPressKeyCode; + this.longPressTimeoutTrigger = longPressTimeoutTrigger; this.keyIconResourceId = keyIconResourceId; - this.keyCharacter = keyCharacter; - this.keyBackgroundDrawableType = keyBackgroundDrawableType; + this.keyCharacter = Preconditions.checkNotNull(keyCharacter); this.flickHighlightEnabled = flickHighlightEnabled; - this.popUp = popUp; + this.popUp = Preconditions.checkNotNull(popUp); + this.horizontalPadding = horizontalPadding; + this.verticalPadding = verticalPadding; + this.iconWidth = iconWidth; + this.iconHeight = iconHeight; } public int getSourceId() { @@ -92,34 +91,55 @@ public int getLongPressKeyCode() { return longPressKeyCode; } + public boolean isLongPressTimeoutTrigger() { + return longPressTimeoutTrigger; + } + public int getKeyIconResourceId() { return keyIconResourceId; } - public String getKeyCharacter() { + public Optional getKeyCharacter() { return keyCharacter; } - public DrawableType getKeyBackgroundDrawableType() { - return keyBackgroundDrawableType; - } - public boolean isFlickHighlightEnabled() { return flickHighlightEnabled; } - public PopUp getPopUp() { + public Optional getPopUp() { return popUp; } + public int getHorizontalPadding() { + return horizontalPadding; + } + + public int getVerticalPadding() { + return verticalPadding; + } + + public int getIconWidth() { + return iconWidth; + } + + public int getIconHeight() { + return iconHeight; + } + @Override public String toString() { return Objects.toStringHelper(this) .add("sourceId", sourceId) .add("keyCode", keyCode) .add("longPressKeyCode", longPressKeyCode) + .add("longPressTimeoutTrigger", longPressTimeoutTrigger) .add("keyIconResourceId", keyIconResourceId) .add("keyCharacter", keyCharacter) + .add("horizontalPadding", horizontalPadding) + .add("verticalPadding", verticalPadding) + .add("iconWidth", iconWidth) + .add("iconHeight", iconHeight) .toString(); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java index a8947d9b6..02c3b6c4e 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java @@ -39,8 +39,6 @@ import java.util.Set; -import javax.annotation.Nullable; - /** * This class represents user's one action, e.g., the sequence of: * press -> move -> move -> ... -> move -> release. @@ -49,8 +47,8 @@ * E.g. for user's two finger strokes, two instances will be instantiated. * */ -// TODO(matsuzakit): Get rid of @Nullable. public class KeyEventContext { + final Key key; final int pointerId; private final float pressedX; @@ -62,7 +60,7 @@ public class KeyEventContext { // TODO(hidehiko): Move logging code to an upper layer, e.g., MozcService or ViewManager etc. // after refactoring the architecture. - private TouchAction lastAction = null; + private Optional lastAction = Optional.absent(); private float lastX; private float lastY; private long lastTimestamp; @@ -70,7 +68,9 @@ public class KeyEventContext { private final int keyboardHeight; // This variable will be updated in the callback of long press key event (if necessary). - boolean longPressSent = false; + boolean pastLongPressSentTimeout = false; + + Optional longPressCallback = Optional.absent(); public KeyEventContext(Key key, int pointerId, float pressedX, float pressedY, int keyboardWidth, int keyboardHeight, @@ -95,9 +95,8 @@ float getFlickThresholdSquared() { /** * Returns true iff the point ({@code x}, {@code y}) is contained by the {@code key}'s region. - * This is package private for testing purpose. */ - static boolean isContained(float x, float y, Key key) { + @VisibleForTesting static boolean isContained(float x, float y, Key key) { float relativeX = x - key.getX(); float relativeY = y - key.getY(); return 0 <= relativeX && relativeX < key.getWidth() && @@ -117,75 +116,82 @@ static boolean isFlickable(Key key, Set metaState) { return false; } KeyState keyState = optionalKeyState.get(); - return keyState.getFlick(Flick.Direction.LEFT) != null || - keyState.getFlick(Flick.Direction.UP) != null || - keyState.getFlick(Flick.Direction.RIGHT) != null || - keyState.getFlick(Flick.Direction.DOWN) != null; + return keyState.getFlick(Flick.Direction.LEFT).isPresent() || + keyState.getFlick(Flick.Direction.UP).isPresent() || + keyState.getFlick(Flick.Direction.RIGHT).isPresent() || + keyState.getFlick(Flick.Direction.DOWN).isPresent(); } /** * Returns the key entity corresponding to {@code metaState} and {@code direction}. */ - @Nullable - public static KeyEntity getKeyEntity(Key key, Set metaState, - @Nullable Flick.Direction direction) { + public static Optional getKeyEntity(Key key, Set metaState, + Optional direction) { Preconditions.checkNotNull(key); Preconditions.checkNotNull(metaState); + Preconditions.checkNotNull(direction); if (key.isSpacer()) { - return null; + return Optional.absent(); } // Key is not spacer for at least one KeyState is available. - return getKeyEntityInternal(key.getKeyState(metaState).get(), direction).orNull(); + return getKeyEntityInternal(key.getKeyState(metaState).get(), direction); } private Optional getKeyEntity(Flick.Direction direction) { return keyState.isPresent() - ? getKeyEntityInternal(keyState.get(), direction) + ? getKeyEntityInternal(keyState.get(), Optional.of(direction)) : Optional.absent(); } private static Optional getKeyEntityInternal(KeyState keyState, - @Nullable Flick.Direction direction) { + Optional direction) { Preconditions.checkNotNull(keyState); + Preconditions.checkNotNull(direction); - if (direction == null) { + if (!direction.isPresent()) { return Optional.absent(); } - Flick flick = keyState.getFlick(direction); - return flick == null ? Optional.absent() : Optional.of(flick.getKeyEntity()); + Optional flick = keyState.getFlick(direction.get()); + return flick.isPresent() + ? Optional.of(flick.get().getKeyEntity()) + : Optional.absent(); } /** * Returns the key code to be sent via {@link KeyboardActionListener#onKey(int, java.util.List)}. + *

    + * If {@code keyEntyty} doesn't trigger longpress by timeout (isLongPressTimeoutTrigger is false), + * the result depends on the timestamp of touch-down event. */ public int getKeyCode() { - if (longPressSent) { + Optional keyEntity = getKeyEntity(flickDirection); + if (!keyEntity.isPresent() + || (pastLongPressSentTimeout && keyEntity.get().isLongPressTimeoutTrigger())) { // If the long-press-key event is already sent, just return INVALID_KEY_CODE. return KeyEntity.INVALID_KEY_CODE; } - - Optional keyEntity = getKeyEntity(flickDirection); - return keyEntity.isPresent() - ? keyEntity.get().getKeyCode() - : KeyEntity.INVALID_KEY_CODE; + return !keyEntity.get().isLongPressTimeoutTrigger() + && keyEntity.get().getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE + && pastLongPressSentTimeout + ? keyEntity.get().getLongPressKeyCode() : keyEntity.get().getKeyCode(); } Set getNextMetaStates(Set originalMetaStates) { + Preconditions.checkNotNull(originalMetaStates); if (!key.isModifier() || key.isSpacer()) { // Non-modifier key shouldn't change meta state. return originalMetaStates; } - Set result = keyState.get().getNextMetaStates(originalMetaStates); - return result; + return keyState.get().getNextMetaStates(originalMetaStates); } /** * Returns the key code to be sent for long press event. */ int getLongPressKeyCode() { - if (longPressSent) { + if (pastLongPressSentTimeout) { // If the long-press-key event is already sent, just return INVALID_KEY_CODE. return KeyEntity.INVALID_KEY_CODE; } @@ -197,6 +203,11 @@ int getLongPressKeyCode() { : KeyEntity.INVALID_KEY_CODE; } + boolean isLongPressTimeoutTrigger() { + Optional keyEntity = getKeyEntity(Flick.Direction.CENTER); + return !keyEntity.isPresent() || keyEntity.get().isLongPressTimeoutTrigger(); + } + /** * Returns the key code to be send via {@link KeyboardActionListener#onPress(int)} and * {@link KeyboardActionListener#onRelease(int)}. @@ -212,20 +223,20 @@ public int getPressedKeyCode() { * Returns true if this key event sequence represents toggling meta state. */ boolean isMetaStateToggleEvent() { - return !longPressSent && key.isModifier() && flickDirection == Flick.Direction.CENTER; + return !pastLongPressSentTimeout && key.isModifier() + && flickDirection == Flick.Direction.CENTER; } /** * Returns the pop up data for the current state. */ - @Nullable - PopUp getCurrentPopUp() { - if (longPressSent) { - return null; + Optional getCurrentPopUp() { + if (pastLongPressSentTimeout) { + return Optional.absent(); } Optional keyEntity = getKeyEntity(flickDirection); - return keyEntity.isPresent() ? keyEntity.get().getPopUp() : null; + return keyEntity.isPresent() ? keyEntity.get().getPopUp() : Optional.absent(); } /** @@ -234,8 +245,9 @@ PopUp getCurrentPopUp() { * @return {@code true} if the internal state is actually updated. */ public boolean update(float x, float y, TouchAction touchAction, long timestamp) { + lastAction = Optional.of(touchAction); + Flick.Direction originalDirection = flickDirection; - lastAction = touchAction; lastX = x; lastY = y; lastTimestamp = timestamp; @@ -257,28 +269,41 @@ public boolean update(float x, float y, TouchAction touchAction, long timestamp) flickDirection = deltaX > 0 ? Flick.Direction.RIGHT : Flick.Direction.LEFT; } } - return flickDirection != originalDirection; + + if (flickDirection == originalDirection) { + return false; + } else { + // If flickDirection has been updated, reset pastLongPressSentTimeout flag + // so that long-press even can be sent again. + // This happens when + // [Hold 'q' key] + // -> [Popup '1' is shown as the result of long-press] + // -> [Flick outside to dismiss the popup] + // -> [Flick again to the center position and hold] + // -> [Popup '1' is shown again as the result of long-press] + pastLongPressSentTimeout = false; + return true; + } } /** * @return {@code TouchEvent} instance which includes the stroke related to this context. */ - @Nullable - public TouchEvent getTouchEvent() { + public Optional getTouchEvent() { Optional keyEntity = getKeyEntity(flickDirection); if (!keyEntity.isPresent()) { - return null; + return Optional.absent(); } TouchEvent.Builder builder = TouchEvent.newBuilder() .setSourceId(keyEntity.get().getSourceId()); builder.addStroke(createTouchPosition( TouchAction.TOUCH_DOWN, pressedX, pressedY, keyboardWidth, keyboardHeight, 0)); - if (lastAction != null) { + if (lastAction.isPresent()) { builder.addStroke(createTouchPosition( - lastAction, lastX, lastY, keyboardWidth, keyboardHeight, lastTimestamp)); + lastAction.get(), lastX, lastY, keyboardWidth, keyboardHeight, lastTimestamp)); } - return builder.build(); + return Optional.of(builder.build()); } public static TouchPosition createTouchPosition( @@ -290,4 +315,8 @@ public static TouchPosition createTouchPosition( .setTimestamp(timestamp) .build(); } + + public void setLongPressCallback(Runnable longPressCallback) { + this.longPressCallback = Optional.of(longPressCallback); + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java index 7d5c9fb40..3ab306fb0 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java @@ -31,6 +31,7 @@ import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import android.os.Handler; import android.os.Looper; @@ -44,13 +45,13 @@ * */ public class KeyEventHandler implements Handler.Callback { + // A dummy argument which is passed to the callback's message. private static final int DUMMY_ARG = 0; // Keys to figure out what message is sent in the callback. - // Package private for testing purpose. - static final int REPEAT_KEY = 1; - static final int LONG_PRESS_KEY = 2; + @VisibleForTesting static final int REPEAT_KEY = 1; + @VisibleForTesting static final int LONG_PRESS_KEY = 2; @VisibleForTesting final Handler handler; private final KeyboardActionListener keyboardActionListener; @@ -67,14 +68,8 @@ public class KeyEventHandler implements Handler.Callback { public KeyEventHandler( Looper looper, KeyboardActionListener keyboardActionListener, int repeatKeyDelay, int repeatKeyInterval, int longPressKeyDelay) { - if (looper == null) { - throw new NullPointerException("looper is null."); - } - if (keyboardActionListener == null) { - throw new NullPointerException("keyboardActionListener is null."); - } - this.handler = new Handler(looper, this); - this.keyboardActionListener = keyboardActionListener; + this.handler = new Handler(Preconditions.checkNotNull(looper), this); + this.keyboardActionListener = Preconditions.checkNotNull(keyboardActionListener); this.repeatKeyDelay = repeatKeyDelay; this.repeatKeyInterval = repeatKeyInterval; this.longPressKeyDelay = longPressKeyDelay; @@ -82,25 +77,51 @@ public KeyEventHandler( @Override public boolean handleMessage(Message message) { - int keyCode = message.arg1; KeyEventContext context = KeyEventContext.class.cast(message.obj); - keyboardActionListener.onKey(keyCode, Collections.singletonList(context.getTouchEvent())); - switch (message.what) { case REPEAT_KEY: { - Message newMessage = handler.obtainMessage(REPEAT_KEY, keyCode, DUMMY_ARG, context); - handler.sendMessageDelayed(newMessage, repeatKeyInterval); + handleMessageRepeatKey(context); break; } case LONG_PRESS_KEY: - // Set a flag that means long-press-key-event has been sent. - context.longPressSent = true; + handleMessageLongPress(context); break; } return true; } + private void handleMessageRepeatKey(KeyEventContext context) { + int keyCode = context.getPressedKeyCode(); + // TODO(hsumita): confirm that we can put null as a touch event or not. + keyboardActionListener.onKey( + keyCode, Collections.singletonList(context.getTouchEvent().orNull())); + Message newMessage = handler.obtainMessage(REPEAT_KEY, keyCode, DUMMY_ARG, context); + handler.sendMessageDelayed(newMessage, repeatKeyInterval); + } + + /** + * Does the things which should be done when long-press operation is done. + *

    + * This is public because this is called from KeyboardView directory in order to implement + * accessibility feature. + */ + public void handleMessageLongPress(KeyEventContext context) { + int keyCode = context.getLongPressKeyCode(); + if (context.isLongPressTimeoutTrigger()) { + // TODO(hsumita): confirm that we can put null as a touch event or not. + keyboardActionListener.onKey( + keyCode, Collections.singletonList(context.getTouchEvent().orNull())); + } + // Callback a function if present then flip the flag for long-press timeout. + // If isLongPressTimeoutTrigger is true, key-code for long-press has already been sent. + // If false, touch-up event for the context will send long-press key code. + if (context.longPressCallback.isPresent()) { + context.longPressCallback.get().run(); + } + context.pastLongPressSentTimeout = true; + } + public void sendPress(int keyCode) { keyboardActionListener.onPress(keyCode); } @@ -109,7 +130,7 @@ public void sendRelease(int keyCode) { keyboardActionListener.onRelease(keyCode); } - public void sendKey(int keyCode, List touchEventList) { + public void sendKey(int keyCode, List touchEventList) { keyboardActionListener.onKey(keyCode, touchEventList); } @@ -117,9 +138,8 @@ public void sendCancel() { keyboardActionListener.onCancel(); } - private void startDelayedKeyEventInternal( - int what, int keyCode, KeyEventContext context, int delay) { - Message message = handler.obtainMessage(what, keyCode, DUMMY_ARG, context); + private void startDelayedKeyEventInternal(int what, KeyEventContext context, int delay) { + Message message = handler.obtainMessage(what, DUMMY_ARG, DUMMY_ARG, context); handler.sendMessageDelayed(message, delay); } @@ -128,16 +148,16 @@ private void startDelayedKeyEventInternal( * based on the given {@code context}. */ public void maybeStartDelayedKeyEvent(KeyEventContext context) { - Key key = context.key; + Key key = Preconditions.checkNotNull(context).key; if (key.isRepeatable()) { int keyCode = context.getPressedKeyCode(); if (keyCode != KeyEntity.INVALID_KEY_CODE) { - startDelayedKeyEventInternal(REPEAT_KEY, keyCode, context, repeatKeyDelay); + startDelayedKeyEventInternal(REPEAT_KEY, context, repeatKeyDelay); } } else { int longPressKeyCode = context.getLongPressKeyCode(); if (longPressKeyCode != KeyEntity.INVALID_KEY_CODE) { - startDelayedKeyEventInternal(LONG_PRESS_KEY, longPressKeyCode, context, longPressKeyDelay); + startDelayedKeyEventInternal(LONG_PRESS_KEY, context, longPressKeyDelay); } } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java index bcc55e9fd..81e488bf8 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java @@ -30,11 +30,13 @@ package org.mozc.android.inputmethod.japanese.keyboard; import com.google.common.base.Objects; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; @@ -51,6 +53,7 @@ * */ public class KeyState { + public enum MetaState { SHIFT(1, true), CAPS_LOCK(2, false), @@ -76,13 +79,22 @@ public enum MetaState { // TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS, TYPE_TEXT_VARIATION_EMAIL_ADDRESS VARIATION_EMAIL_ADDRESS(2048, false), - // Set if Globe button is enabled by preference. - // TODO(matsuzakit): Implement me. + // Set if Globe button should be offered. + // c.f., SubtypeImeSwitcher#shouldOfferSwitchingToNextInputMethod() GLOBE(4096, false), + // !GLOBE + NO_GLOBE(8192, false), // Set if there is composition string. - // TODO(matsuzakit): Implement me. This is mandatory when implementing ACTION_*. - COMPOSING(8192, false), + COMPOSING(16384, false), + // Set if the KeyboardView is handling at least one touch event. + HANDLING_TOUCH_EVENT(32768, false), + + // DO NOT USE. Theoretically this is package private entry. + // "fallback" flag is useful when defining .xml file with logical-OR operator + // (e.g. "fallback|composing"). + // This entry is used just for it. + FALLBACK(1073741824, false), ; private final int bitFlag; @@ -116,11 +128,17 @@ public static MetaState valueOf(int bitFlag) { // MetaStates for text variations. public static final Set VARIATION_EXCLUSIVE_GROUP = Sets.immutableEnumSet(MetaState.VARIATION_URI, MetaState.VARIATION_EMAIL_ADDRESS); + // MetaStates for Globe icon. + public static final Set GLOBE_EXCLUSIVE_OR_GROUP = + Sets.immutableEnumSet(MetaState.GLOBE, MetaState.NO_GLOBE); @SuppressWarnings("unchecked") private static final Collection> EXCLUSIVE_GROUP = Arrays.>asList(CHAR_TYPE_EXCLUSIVE_GROUP, ACTION_EXCLUSIVE_GROUP, - VARIATION_EXCLUSIVE_GROUP); + VARIATION_EXCLUSIVE_GROUP, + GLOBE_EXCLUSIVE_OR_GROUP); + private static final Collection> OR_GROUP = + Collections.singleton(GLOBE_EXCLUSIVE_OR_GROUP); /** * Checks if {@code testee} is valid set. @@ -129,6 +147,8 @@ public static MetaState valueOf(int bitFlag) { * Do not call from chokepoint. */ public static boolean isValidSet(Set testee) { + Preconditions.checkNotNull(testee); + // Set#retainAll can make the implementation simpler, but it requires instantiation of // (Enum)Set for each iteration. for (Set exclusiveGroup : EXCLUSIVE_GROUP) { @@ -142,6 +162,11 @@ public static boolean isValidSet(Set testee) { } } } + for (Set orGroup : OR_GROUP) { + if (orGroup.isEmpty()) { + return false; + } + } return true; } } @@ -160,14 +185,13 @@ public KeyState(String contentDescription, this.contentDescription = Preconditions.checkNotNull(contentDescription); Preconditions.checkNotNull(metaStates); this.metaState = Sets.newEnumSet(metaStates, MetaState.class); - this.nextAddMetaStates = nextAddMetaStates; - this.nextRemoveMetaStates = nextRemoveMetaStates; + this.nextAddMetaStates = Preconditions.checkNotNull(nextAddMetaStates); + this.nextRemoveMetaStates = Preconditions.checkNotNull(nextRemoveMetaStates); this.flickMap = new EnumMap(Flick.Direction.class); - for (Flick flick : flickCollection) { - if (this.flickMap.put(flick.getDirection(), flick) != null) { - throw new IllegalArgumentException( - "Duplicate flick direction is found: " + flick.getDirection()); - } + for (Flick flick : Preconditions.checkNotNull(flickCollection)) { + Preconditions.checkArgument( + this.flickMap.put(flick.getDirection(), flick) == null, + "Duplicate flick direction is found: " + flick.getDirection()); } } @@ -188,12 +212,12 @@ public Set getMetaStateSet() { * The result is "valid" in the light of {@code MetaState#isValidSet(Set)}. */ public Set getNextMetaStates(Set originalMetaStates) { - return Sets.union(Sets.difference(originalMetaStates, + return Sets.union(Sets.difference(Preconditions.checkNotNull(originalMetaStates), nextRemoveMetaStates), nextAddMetaStates).immutableCopy(); } - public Flick getFlick(Flick.Direction direction) { - return flickMap.get(direction); + public Optional getFlick(Flick.Direction direction) { + return Optional.fromNullable(flickMap.get(direction)); } @Override diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java index 50c89104b..b12e1d926 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java @@ -29,9 +29,18 @@ package org.mozc.android.inputmethod.japanese.keyboard; +import org.mozc.android.inputmethod.japanese.KeyboardSpecificationName; +import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable; +import org.mozc.android.inputmethod.japanese.resources.R; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import android.util.SparseIntArray; + import java.util.Collections; import java.util.List; @@ -42,6 +51,231 @@ */ public class Keyboard { + /** + * Each keyboard has its own specification. + * + * For example, some keyboards use a special Romanji table. + */ + public static enum KeyboardSpecification { + // 12 keys. + TWELVE_KEY_TOGGLE_KANA( + new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_KANA", 0, 2, 0), + R.xml.kbd_12keys_kana, + false, + CompositionMode.HIRAGANA, + SpecialRomanjiTable.TWELVE_KEYS_TO_HIRAGANA, + SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, + true, + CrossingEdgeBehavior.DO_NOTHING), + + TWELVE_KEY_TOGGLE_ALPHABET( + new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_ALPHABET", 0, 2, 0), + R.xml.kbd_12keys_abc, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.TWELVE_KEYS_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.DO_NOTHING), + + TWELVE_KEY_TOGGLE_QWERTY_ALPHABET( + new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_QWERTY_ALPHABET", 0, 5, 0), + R.xml.kbd_qwerty_abc, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), + + // Flick mode. + TWELVE_KEY_FLICK_KANA( + new KeyboardSpecificationName("TWELVE_KEY_FLICK_KANA", 0, 2, 0), + R.xml.kbd_12keys_flick_kana, + false, + CompositionMode.HIRAGANA, + SpecialRomanjiTable.FLICK_TO_HIRAGANA, + SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, + true, + CrossingEdgeBehavior.DO_NOTHING), + + TWELVE_KEY_FLICK_ALPHABET( + new KeyboardSpecificationName("TWELVE_KEY_FLICK_ALPHABET", 0, 2, 0), + R.xml.kbd_12keys_flick_abc, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.FLICK_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), + + TWELVE_KEY_TOGGLE_FLICK_KANA( + new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_KANA", 0, 2, 0), + R.xml.kbd_12keys_flick_kana, + false, + CompositionMode.HIRAGANA, + SpecialRomanjiTable.TOGGLE_FLICK_TO_HIRAGANA, + SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, + true, + CrossingEdgeBehavior.DO_NOTHING), + + TWELVE_KEY_TOGGLE_FLICK_ALPHABET( + new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 2, 0), + R.xml.kbd_12keys_flick_abc, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.TOGGLE_FLICK_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.DO_NOTHING), + + // QWERTY keyboard. + QWERTY_KANA( + new KeyboardSpecificationName("QWERTY_KANA", 0, 4, 0), + R.xml.kbd_qwerty_kana, + false, + CompositionMode.HIRAGANA, + SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA, + SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, + false, + CrossingEdgeBehavior.DO_NOTHING), + + QWERTY_ALPHABET( + new KeyboardSpecificationName("QWERTY_ALPHABET", 0, 5, 0), + R.xml.kbd_qwerty_abc, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), + + QWERTY_ALPHABET_NUMBER( + new KeyboardSpecificationName("QWERTY_ALPHABET_NUMBER", 0, 3, 0), + R.xml.kbd_qwerty_abc_123, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), + + // Godan keyboard. + GODAN_KANA( + new KeyboardSpecificationName("GODAN_KANA", 0, 2, 0), + R.xml.kbd_godan_kana, + false, + CompositionMode.HIRAGANA, + SpecialRomanjiTable.GODAN_TO_HIRAGANA, + SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, + true, + CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING), + + NUMBER( + new KeyboardSpecificationName("NUMBER", 0, 1, 0), + R.xml.kbd_123, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.DO_NOTHING), + + // Number keyboard on symbol input view. + SYMBOL_NUMBER( + new KeyboardSpecificationName("TWELVE_KEY_SYMBOL_NUMBER", 0, 1, 0), + R.xml.kbd_symbol_123, + false, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.DO_NOTHING), + + // HARDWARE QWERTY keyboard. + HARDWARE_QWERTY_KANA( + new KeyboardSpecificationName("HARDWARE_QWERTY_KANA", 0, 1, 0), + 0, + true, + CompositionMode.HIRAGANA, + SpecialRomanjiTable.DEFAULT_TABLE, + SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION, + false, + CrossingEdgeBehavior.DO_NOTHING), + + HARDWARE_QWERTY_ALPHABET( + new KeyboardSpecificationName("HARDWARE_QWERTY_ALPHABET", 0, 1, 0), + 0, + true, + CompositionMode.HALF_ASCII, + SpecialRomanjiTable.DEFAULT_TABLE, + SpaceOnAlphanumeric.COMMIT, + false, + CrossingEdgeBehavior.DO_NOTHING), + + ; + + private final KeyboardSpecificationName specName; + private final int resourceId; + private final boolean isHardwareKeyboard; + private final CompositionMode compositionMode; + private final SpecialRomanjiTable specialRomanjiTable; + private final SpaceOnAlphanumeric spaceOnAlphanumeric; + private final boolean kanaModifierInsensitiveConversion; + private final CrossingEdgeBehavior crossingEdgeBehavior; + + private KeyboardSpecification( + KeyboardSpecificationName specName, + int resourceId, + boolean isHardwareKeyboard, + CompositionMode compositionMode, + SpecialRomanjiTable specialRomanjiTable, + SpaceOnAlphanumeric spaceOnAlphanumeric, + boolean kanaModifierInsensitiveConversion, + CrossingEdgeBehavior crossingEdgeBehavior) { + this.specName = Preconditions.checkNotNull(specName); + this.resourceId = resourceId; + this.isHardwareKeyboard = isHardwareKeyboard; + this.compositionMode = Preconditions.checkNotNull(compositionMode); + this.specialRomanjiTable = Preconditions.checkNotNull(specialRomanjiTable); + this.spaceOnAlphanumeric = Preconditions.checkNotNull(spaceOnAlphanumeric); + this.kanaModifierInsensitiveConversion = kanaModifierInsensitiveConversion; + this.crossingEdgeBehavior = Preconditions.checkNotNull(crossingEdgeBehavior); + } + + public int getXmlLayoutResourceId() { + return resourceId; + } + + public CompositionMode getCompositionMode() { + return compositionMode; + } + + public KeyboardSpecificationName getKeyboardSpecificationName() { + return specName; + } + + public SpecialRomanjiTable getSpecialRomanjiTable() { + return specialRomanjiTable; + } + + public SpaceOnAlphanumeric getSpaceOnAlphanumeric() { + return spaceOnAlphanumeric; + } + + public boolean isKanaModifierInsensitiveConversion() { + return kanaModifierInsensitiveConversion; + } + + public CrossingEdgeBehavior getCrossingEdgeBehavior() { + return crossingEdgeBehavior; + } + + public boolean isHardwareKeyboard() { + return isHardwareKeyboard; + } + } + private final Optional contentDescription; private final float flickThreshold; private final List rowList; @@ -50,9 +284,12 @@ public class Keyboard { public final int contentRight; public final int contentTop; public final int contentBottom; + protected final KeyboardSpecification specification; + private Optional sourceIdToKeyCode = Optional.absent(); public Keyboard(Optional contentDescription, - List rowList, float flickThreshold) { + List rowList, float flickThreshold, + KeyboardSpecification specification) { this.contentDescription = Preconditions.checkNotNull(contentDescription); this.flickThreshold = flickThreshold; this.rowList = Collections.unmodifiableList(rowList); @@ -71,6 +308,7 @@ public Keyboard(Optional contentDescription, this.contentRight = right; this.contentTop = top; this.contentBottom = bottom; + this.specification = Preconditions.checkNotNull(specification); } public Optional getContentDescription() { @@ -84,4 +322,39 @@ public float getFlickThreshold() { public List getRowList() { return rowList; } + + public KeyboardSpecification getSpecification() { + return specification; + } + + /** + * Returns keyCode from {@code souceId}. + * + *

    If not found, {@code Integer.MIN_VALUE} is returned. + */ + public int getKeyCode(int sourceId) { + ensureSourceIdToKeyCode(); + return sourceIdToKeyCode.get().get(sourceId, Integer.MIN_VALUE); + } + + private void ensureSourceIdToKeyCode() { + if (sourceIdToKeyCode.isPresent()) { + return; + } + SparseIntArray result = new SparseIntArray(); + for (Row row : getRowList()) { + for (Key key : row.getKeyList()) { + for (KeyState keyState : key.getKeyStates()) { + for (Direction direction : Direction.values()) { + Optional flick = keyState.getFlick(direction); + if (flick.isPresent()) { + KeyEntity keyEntity = flick.get().getKeyEntity(); + result.put(keyEntity.getSourceId(), keyEntity.getKeyCode()); + } + } + } + } + } + sourceIdToKeyCode = Optional.of(result); + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java index bdc8a5ca7..51ee9e392 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java @@ -35,11 +35,11 @@ /** * A listener of keyboard actions. - * + * */ public interface KeyboardActionListener { public void onCancel(); public void onPress(int keycode); public void onRelease(int keycode); - public void onKey(int primaryCode, List touchEventList); + public void onKey(int primaryCode, List touchEventList); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardFactory.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardFactory.java similarity index 82% rename from src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardFactory.java rename to src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardFactory.java index 106ea5883..3f38a5f35 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardFactory.java @@ -27,10 +27,12 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -package org.mozc.android.inputmethod.japanese; +package org.mozc.android.inputmethod.japanese.keyboard; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; +import org.mozc.android.inputmethod.japanese.MozcLog; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.util.LeastRecentlyUsedCacheMap; +import com.google.common.base.Optional; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; @@ -38,13 +40,14 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.Collections; import java.util.Map; /** * Factory of the keyboard data based on xml. * */ -public class JapaneseKeyboardFactory { +public class KeyboardFactory { /** * Key for the cache map of keyboard. @@ -85,8 +88,8 @@ public int hashCode() { */ private static final int CACHE_SIZE = 6; - private final Map cache = - new LeastRecentlyUsedCacheMap(CACHE_SIZE); + private final Map cache = + new LeastRecentlyUsedCacheMap(CACHE_SIZE); /** * @return JapaneseKeyboard instance based on given resources and specification. @@ -96,8 +99,8 @@ public int hashCode() { * @throws NullPointerException if given {@code resources} or {@code specification} is * {@code null}. */ - public JapaneseKeyboard get(Resources resources, KeyboardSpecification specification, - int keyboardWidth, int keyboardHeight) { + public Keyboard get(Resources resources, KeyboardSpecification specification, + int keyboardWidth, int keyboardHeight) { if (resources == null) { throw new NullPointerException("resources is null."); } @@ -109,7 +112,7 @@ public JapaneseKeyboard get(Resources resources, KeyboardSpecification specifica new CacheKey(specification, keyboardWidth, keyboardHeight); // First, look up from the cache. - JapaneseKeyboard keyboard = cache.get(cacheKey); + Keyboard keyboard = cache.get(cacheKey); if (keyboard == null) { // If not found, parse keyboard from a xml resource file. The result will be cached in // the cache map. @@ -121,12 +124,11 @@ public JapaneseKeyboard get(Resources resources, KeyboardSpecification specifica return keyboard; } - private static JapaneseKeyboard parseKeyboard( + private static Keyboard parseKeyboard( Resources resources, KeyboardSpecification specification, int keyboardWidth, int keyboardHeight) { - JapaneseKeyboardParser parser = new JapaneseKeyboardParser( - resources, resources.getXml(specification.getXmlLayoutResourceId()), specification, - keyboardWidth, keyboardHeight); + KeyboardParser parser = new KeyboardParser( + resources, keyboardWidth, keyboardHeight, specification); try { return parser.parseKeyboard(); } catch (NotFoundException e) { @@ -136,9 +138,9 @@ private static JapaneseKeyboard parseKeyboard( } catch (IOException e) { MozcLog.e(e.getMessage()); } - - // Returns null if failed. - return null; + // Returns dummy keyboard to avoid crash. + return new Keyboard(Optional.absent(), Collections.emptyList(), 0, + KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); } /** diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java index 80ac1800e..cd4c57ee6 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java @@ -31,7 +31,9 @@ import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; import org.mozc.android.inputmethod.japanese.keyboard.Key.Stick; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.resources.R; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Preconditions; @@ -59,35 +61,172 @@ public class KeyboardParser { /** Attributes for the key dimensions. */ - private static class KeyAttributes { + @VisibleForTesting static class KeyAttributes { + + @VisibleForTesting static class Builder { + + private int width; + private int height; + private int horizontalLayoutWeight; + private int horizontalGap; + private int verticalGap; + private int defaultIconWidth; + private int defaultIconHeight; + private int defaultHorizontalPadding; + private int defaultVerticalPadding; + private DrawableType keyBackgroundDrawableType = + DrawableType.TWELVEKEYS_REGULAR_KEY_BACKGROUND; + private int edgeFlags; + private boolean isRepeatable; + private boolean isModifier; + private Stick stick = Stick.EVEN; + private List keyStateList = Collections.emptyList(); + + Builder setWidth(int width) { + this.width = width; + return this; + } + Builder setHeight(int height) { + this.height = height; + return this; + } + Builder setHorizontalLayoutWeight(int horizontalLayoutWeight) { + this.horizontalLayoutWeight = horizontalLayoutWeight; + return this; + } + Builder setHorizontalGap(int horizontalGap) { + this.horizontalGap = horizontalGap; + return this; + } + Builder setVerticalGap(int verticalGap) { + this.verticalGap = verticalGap; + return this; + } + Builder setDefaultIconWidth(int defaultIconWidth) { + this.defaultIconWidth = defaultIconWidth; + return this; + } + Builder setDefaultIconHeight(int defaultIconHeight) { + this.defaultIconHeight = defaultIconHeight; + return this; + } + Builder setDefaultHorizontalPadding(int defaultHorizontalPadding) { + this.defaultHorizontalPadding = defaultHorizontalPadding; + return this; + } + Builder setDefaultVerticalPadding(int defaultVerticalPadding) { + this.defaultVerticalPadding = defaultVerticalPadding; + return this; + } + Builder setKeybackgroundDrawableType(DrawableType type) { + this.keyBackgroundDrawableType = Preconditions.checkNotNull(type); + return this; + } + Builder setEdgeFlags(int edgeFlags) { + this.edgeFlags = edgeFlags; + return this; + } + Builder setRepeatable(boolean isRepeatable) { + this.isRepeatable = isRepeatable; + return this; + } + Builder setModifier(boolean isModifier) { + this.isModifier = isModifier; + return this; + } + Builder setStick(Stick stick) { + this.stick = Preconditions.checkNotNull(stick); + return this; + } + Builder setKeyStateList(List keyStateList) { + this.keyStateList = Preconditions.checkNotNull(keyStateList); + return this; + } + KeyAttributes build() { + return new KeyAttributes(this); + } + } + final int width; final int height; + final int horizontalLayoutWeight; final int horizontalGap; final int verticalGap; - DrawableType keyBackgroundDrawableType; + final int defaultIconWidth; + final int defaultIconHeight; + final int defaultHorizontalPadding; + final int defaultVerticalPadding; + final DrawableType keyBackgroundDrawableType; + final int edgeFlags; + final boolean isRepeatable; + final boolean isModifier; + final Stick stick; + final List keyStateList; + + private KeyAttributes(Builder builder) { + this.width = builder.width; + this.height = builder.height; + this.horizontalLayoutWeight = builder.horizontalLayoutWeight; + this.horizontalGap = builder.horizontalGap; + this.verticalGap = builder.verticalGap; + this.defaultIconWidth = builder.defaultIconWidth; + this.defaultIconHeight = builder.defaultIconHeight; + this.defaultHorizontalPadding = builder.defaultHorizontalPadding; + this.defaultVerticalPadding = builder.defaultVerticalPadding; + this.keyBackgroundDrawableType = + Preconditions.checkNotNull(builder.keyBackgroundDrawableType); + this.edgeFlags = builder.edgeFlags; + this.isRepeatable = builder.isRepeatable; + this.isModifier = builder.isModifier; + this.stick = builder.stick; + this.keyStateList = builder.keyStateList; + } + + static Builder newBuilder() { + return new Builder(); + } - KeyAttributes(int width, int height, int horizontalGap, int verticalGap, - DrawableType keyBackgroundDrawableType) { - this.width = width; - this.height = height; - this.horizontalGap = horizontalGap; - this.verticalGap = verticalGap; - this.keyBackgroundDrawableType = keyBackgroundDrawableType; + Builder toBuilder() { + return newBuilder() + .setWidth(width) + .setHeight(height) + .setHorizontalLayoutWeight(horizontalLayoutWeight) + .setHorizontalGap(horizontalGap) + .setVerticalGap(verticalGap) + .setDefaultHorizontalPadding(defaultHorizontalPadding) + .setDefaultVerticalPadding(defaultVerticalPadding) + .setDefaultIconWidth(defaultIconWidth) + .setDefaultIconHeight(defaultIconHeight) + .setKeybackgroundDrawableType(keyBackgroundDrawableType) + .setEdgeFlags(edgeFlags) + .setRepeatable(isRepeatable) + .setModifier(isModifier) + .setStick(stick) + .setKeyStateList(keyStateList); + } + + Key buildKey(int x, int y, int width) { + return new Key(x, y, width, height, horizontalGap, edgeFlags, + isRepeatable, isModifier, stick, keyBackgroundDrawableType, keyStateList); } } /** Attributes for the popup dimensions. */ private static class PopUpAttributes { - final int popUpWidth; + final int popUpHeight; final int popUpXOffset; final int popUpYOffset; + final int popUpIconWidth; + final int popUpIconHeight; - PopUpAttributes(int popUpWidth, int popUpHeight, int popUpXOffset, int popUpYOffset) { - this.popUpWidth = popUpWidth; + PopUpAttributes(int popUpHeight, int popUpXOffset, int popUpYOffset, + int popUpIconWidth, int popUpIconHeight) { this.popUpHeight = popUpHeight; this.popUpXOffset = popUpXOffset; this.popUpYOffset = popUpYOffset; + this.popUpIconWidth = popUpIconWidth; + this.popUpIconHeight = popUpIconHeight; } } @@ -115,16 +254,47 @@ private static class PopUpAttributes { private static final int ROW_ROW_EDGE_FLAGS_INDEX = Arrays.binarySearch(ROW_ATTRIBUTES, R.attr.rowEdgeFlags); + private static final int[] POPUP_ATTRIBUTES = { + R.attr.popUpIcon, + R.attr.popUpLongPressIcon, + R.attr.popUpHeight, + R.attr.popUpXOffset, + R.attr.popUpYOffset, + R.attr.popUpIconWidth, + R.attr.popUpIconHeight, + }; + static { + Arrays.sort(POPUP_ATTRIBUTES); + } + private static final int POPUP_KEY_ICON_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpIcon); + private static final int POPUP_KEY_LONG_PRESS_ICON_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpLongPressIcon); + private static final int POPUP_KEY_HEIGHT_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpHeight); + private static final int POPUP_KEY_X_OFFSET_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpXOffset); + private static final int POPUP_KEY_Y_OFFSET_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpYOffset); + private static final int POPUP_KEY_ICON_WIDTH_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpIconWidth); + private static final int POPUP_KEY_ICON_HEIGHT_INDEX = + Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpIconHeight); + /** Attributes for a {@code } element. */ private static final int[] KEY_ATTRIBUTES = { R.attr.keyWidth, R.attr.keyHeight, + R.attr.keyHorizontalLayoutWeight, R.attr.horizontalGap, + R.attr.defaultIconWidth, + R.attr.defaultIconHeight, + R.attr.defaultHorizontalPadding, + R.attr.defaultVerticalPadding, R.attr.keyBackground, R.attr.keyEdgeFlags, R.attr.isRepeatable, R.attr.isModifier, - R.attr.isSticky, }; static { Arrays.sort(KEY_ATTRIBUTES); @@ -133,8 +303,18 @@ private static class PopUpAttributes { Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyWidth); private static final int KEY_KEY_HEIGHT_INDEX = Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyHeight); + private static final int KEY_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX = + Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyHorizontalLayoutWeight); private static final int KEY_HORIZONTAL_GAP_INDEX = Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.horizontalGap); + private static final int KEY_DEFAULT_ICON_WIDTH_INDEX = + Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultIconWidth); + private static final int KEY_DEFAULT_ICON_HEIGHT_INDEX = + Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultIconHeight); + private static final int KEY_DEFAULT_HORIZONTAL_PADDING_INDEX = + Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultHorizontalPadding); + private static final int KEY_DEFAULT_VERTICAL_PADDING_INDEX = + Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultVerticalPadding); private static final int KEY_KEY_BACKGROUND_INDEX = Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyBackground); private static final int KEY_KEY_EDGE_FLAGS_INDEX = @@ -143,32 +323,35 @@ private static class PopUpAttributes { Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.isRepeatable); private static final int KEY_IS_MODIFIER_INDEX = Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.isModifier); - private static final int KEY_IS_STICKY_INDEX = - Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.isSticky); /** Attributes for a {@code } element. */ private static final int[] SPACER_ATTRIBUTES = { + R.attr.keyWidth, R.attr.keyHeight, - R.attr.horizontalGap, + R.attr.keyHorizontalLayoutWeight, R.attr.keyEdgeFlags, R.attr.stick, + R.attr.keyBackground, }; static { Arrays.sort(SPACER_ATTRIBUTES); } + private static final int SPACER_KEY_WIDTH_INDEX = + Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyWidth); private static final int SPACER_KEY_HEIGHT_INDEX = Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyHeight); - private static final int SPACER_HORIZONTAL_GAP_INDEX = - Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.horizontalGap); + private static final int SPACER_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX = + Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyHorizontalLayoutWeight); private static final int SPACER_KEY_EDGE_FLAGS_INDEX = Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyEdgeFlags); private static final int SPACER_STICK_INDEX = Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.stick); + private static final int SPACER_KEY_BACKGROUND_INDEX = + Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyBackground); /** Attributes for a {@code } element. */ private static final int[] KEY_STATE_ATTRIBUTES = { R.attr.contentDescription, - R.attr.keyBackground, R.attr.metaState, R.attr.nextMetaState, R.attr.nextRemovedMetaStates, @@ -178,8 +361,6 @@ private static class PopUpAttributes { } private static final int KEY_STATE_CONTENT_DESCRIPTION_INDEX = Arrays.binarySearch(KEY_STATE_ATTRIBUTES, R.attr.contentDescription); - private static final int KEY_STATE_KEY_BACKGROUND_INDEX = - Arrays.binarySearch(KEY_STATE_ATTRIBUTES, R.attr.keyBackground); private static final int KEY_STATE_META_STATE_INDEX = Arrays.binarySearch(KEY_STATE_ATTRIBUTES, R.attr.metaState); private static final int KEY_STATE_NEXT_META_STATE_INDEX = @@ -192,9 +373,14 @@ private static class PopUpAttributes { R.attr.sourceId, R.attr.keyCode, R.attr.longPressKeyCode, + R.attr.longPressTimeoutTrigger, R.attr.keyIcon, R.attr.keyCharacter, R.attr.flickHighlight, + R.attr.horizontalPadding, + R.attr.verticalPadding, + R.attr.iconWidth, + R.attr.iconHeight, }; static { Arrays.sort(KEY_ENTITY_ATTRIBUTES); @@ -205,12 +391,22 @@ private static class PopUpAttributes { Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.keyCode); private static final int KEY_ENTITY_LONG_PRESS_KEY_CODE_INDEX = Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.longPressKeyCode); + private static final int KEY_ENTITY_LONG_PRESS_TIMEOUT_TRIGGER_INDEX = + Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.longPressTimeoutTrigger); private static final int KEY_ENTITY_KEY_ICON_INDEX = Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.keyIcon); private static final int KEY_ENTITY_KEY_CHAR_INDEX = Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.keyCharacter); private static final int KEY_ENTITY_FLICK_HIGHLIGHT_INDEX = Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.flickHighlight); + private static final int KEY_ENTITY_HORIZONTAL_PADDING_INDEX = + Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.horizontalPadding); + private static final int KEY_ENTITY_VERTICAL_PADDING_INDEX = + Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.verticalPadding); + private static final int KEY_ENTITY_ICON_WIDTH_INDEX = + Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.iconWidth); + private static final int KEY_ENTITY_ICON_HEIGHT_INDEX = + Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.iconHeight); /** * Mapping table from enum value in xml to DrawableType by using the enum value as index. @@ -222,30 +418,33 @@ private static class PopUpAttributes { DrawableType.QWERTY_REGULAR_KEY_BACKGROUND, DrawableType.QWERTY_FUNCTION_KEY_BACKGROUND, DrawableType.QWERTY_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS, - DrawableType.QWERTY_FUNCTION_KEY_LIGHT_ON_BACKGROUND, - DrawableType.QWERTY_FUNCTION_KEY_LIGHT_OFF_BACKGROUND, + DrawableType.QWERTY_FUNCTION_KEY_SPACE_WITH_THREEDOTS, + DrawableType.KEYBOARD_SEPARATOR_TOP, + DrawableType.KEYBOARD_SEPARATOR_CENTER, + DrawableType.KEYBOARD_SEPARATOR_BOTTOM, + DrawableType.TRNASPARENT, }; /** * @return "sourceId" assigned to {@code value}. */ - static int getSourceId(TypedValue value, @SuppressWarnings("unused") int defaultValue) { - if (value == null || - (value.type != TypedValue.TYPE_INT_DEC && - value.type != TypedValue.TYPE_INT_HEX)) { - throw new IllegalArgumentException("sourceId is mandatory for KeyEntity."); - } + private static int getSourceId(TypedValue value, @SuppressWarnings("unused") int defaultValue) { + Preconditions.checkNotNull(value); + Preconditions.checkArgument( + value.type == TypedValue.TYPE_INT_DEC || value.type == TypedValue.TYPE_INT_HEX, + "sourceId is mandatory for KeyEntity."); return value.data; } /** * @return the pixel offsets based on metrics and base */ - static int getDimensionOrFraction( - TypedValue value, int base, int defaultValue, DisplayMetrics metrics) { - if (value == null) { + @VisibleForTesting static int getDimensionOrFraction( + Optional optionalValue, int base, int defaultValue, DisplayMetrics metrics) { + if (!optionalValue.isPresent()) { return defaultValue; } + TypedValue value = optionalValue.get(); switch (value.type) { case TypedValue.TYPE_DIMENSION: @@ -258,13 +457,29 @@ static int getDimensionOrFraction( " value = " + value.toString()); } + @VisibleForTesting static int getFraction( + Optional optionalValue, int base, int defaultValue) { + if (!optionalValue.isPresent()) { + return defaultValue; + } + TypedValue value = optionalValue.get(); + + if (value.type == TypedValue.TYPE_FRACTION) { + return Math.round(TypedValue.complexToFraction(value.data, base, base)); + } + + throw new IllegalArgumentException( + "The type fraction is required. value = " + value.toString()); + } + /** * @return "codes" assigned to {@code value} */ - static int getCode(TypedValue value, int defaultValue) { - if (value == null) { + @VisibleForTesting static int getCode(Optional optionalValue, int defaultValue) { + if (!optionalValue.isPresent()) { return defaultValue; } + TypedValue value = optionalValue.get(); if (value.type == TypedValue.TYPE_INT_DEC || value.type == TypedValue.TYPE_INT_HEX) { @@ -277,20 +492,6 @@ static int getCode(TypedValue value, int defaultValue) { return defaultValue; } - /** - * A simple wrapper of {@link CharSequence#toString()}, in order to avoid - * {@code NullPointerException}. - * @param sequence input character sequence - * @return {@code sequence.toString()} if {@code sequence} is not {@code null}. - * Otherwise, {@code null}. - */ - static String toStringOrNull(CharSequence sequence) { - if (sequence == null) { - return null; - } - return sequence.toString(); - } - private static void ignoreWhiteSpaceAndComment(XmlPullParser parser) throws XmlPullParserException, IOException { int event = parser.getEventType(); @@ -302,8 +503,8 @@ private static void ignoreWhiteSpaceAndComment(XmlPullParser parser) private static void assertStartDocument(XmlPullParser parser) throws XmlPullParserException { if (parser.getEventType() != XmlPullParser.START_DOCUMENT) { throw new IllegalArgumentException( - "The start of document is expected, but actually not: " + - parser.getPositionDescription()); + "The start of document is expected, but actually not: " + + parser.getPositionDescription()); } } @@ -325,8 +526,8 @@ private static void assertTagName(XmlPullParser parser, String expectedName) { String actualName = parser.getName(); if (!actualName.equals(expectedName)) { throw new IllegalArgumentException( - "Tag <" + expectedName + "> is expected, but found <" + actualName + ">: " + - parser.getPositionDescription()); + "Tag <" + expectedName + "> is expected, but found <" + actualName + ">: " + + parser.getPositionDescription()); } } @@ -353,20 +554,16 @@ private static void assertEndTag(XmlPullParser parser, String expectedName) private final Set sourceIdSet = new HashSet(); private final int keyboardWidth; private final int keyboardHeight; + private final KeyboardSpecification specification; - public KeyboardParser(Resources resources, XmlResourceParser xmlResourceParser, - int keyboardWidth, int keyboardHeight) { - if (resources == null) { - throw new NullPointerException("resources shouldn't be null."); - } - if (xmlResourceParser == null) { - throw new NullPointerException("xmlResourceParser shouldn't be null."); - } - - this.resources = resources; - this.xmlResourceParser = xmlResourceParser; + public KeyboardParser(Resources resources, + int keyboardWidth, int keyboardHeight, + KeyboardSpecification specification) { + this.resources = Preconditions.checkNotNull(resources); this.keyboardWidth = keyboardWidth; this.keyboardHeight = keyboardHeight; + this.specification = Preconditions.checkNotNull(specification); + this.xmlResourceParser = resources.getXml(specification.getXmlLayoutResourceId()); } /** @@ -427,23 +624,38 @@ public Keyboard parseKeyboard() throws XmlPullParserException, IOException { // The default keyWidth is 10% of the display for width, and 50px for height. keyAttributes = parseKeyAttributes( attributes, - new KeyAttributes(keyboardWidth / 10, 50, 0, 0, null), + KeyAttributes.newBuilder() + .setWidth(keyboardWidth / 10) + .setHeight(50) + .setKeybackgroundDrawableType(DrawableType.TWELVEKEYS_REGULAR_KEY_BACKGROUND) + .setDefaultIconWidth(keyboardWidth) + .setDefaultIconHeight(keyboardHeight) + .setDefaultHorizontalPadding(0) + .setDefaultVerticalPadding(0) + .build(), metrics, this.keyboardWidth, this.keyboardHeight, R.styleable.Keyboard_keyWidth, R.styleable.Keyboard_keyHeight, + R.styleable.Keyboard_keyHorizontalLayoutWeight, R.styleable.Keyboard_horizontalGap, R.styleable.Keyboard_verticalGap, + R.styleable.Keyboard_defaultIconWidth, + R.styleable.Keyboard_defaultIconHeight, + R.styleable.Keyboard_defaultHorizontalPadding, + R.styleable.Keyboard_defaultVerticalPadding, R.styleable.Keyboard_keyBackground); popUpAttributes = parsePopUpAttributes( attributes, + new PopUpAttributes(0, 0, 0, 0, 0), metrics, this.keyboardWidth, - R.styleable.Keyboard_popUpWidth, R.styleable.Keyboard_popUpHeight, R.styleable.Keyboard_popUpXOffset, - R.styleable.Keyboard_popUpYOffset); + R.styleable.Keyboard_popUpYOffset, + R.styleable.Keyboard_popUpIconWidth, + R.styleable.Keyboard_popUpIconHeight); flickThreshold = parseFlickThreshold( attributes, R.styleable.Keyboard_flickThreshold); contentDescription = Optional.fromNullable( @@ -473,7 +685,36 @@ public Keyboard parseKeyboard() throws XmlPullParserException, IOException { ignoreWhiteSpaceAndComment(parser); assertEndDocument(parser); - return buildKeyboard(contentDescription, rowList, flickThreshold); + return new Keyboard(Preconditions.checkNotNull(contentDescription), + Preconditions.checkNotNull(rowList), flickThreshold, specification); + } + + @VisibleForTesting + static List buildKeyList(List keyAttributesList, + int y, int rowWidth) { + float remainingWidthByWeight = rowWidth; + int remainingWeight = 0; + for (KeyAttributes attributes : keyAttributesList) { + remainingWidthByWeight -= attributes.width; + remainingWeight += attributes.horizontalLayoutWeight; + } + + List keyList = new ArrayList(keyAttributesList.size()); + float exactX = 0; + for (KeyAttributes attributes : keyAttributesList) { + int weight = attributes.horizontalLayoutWeight; + Preconditions.checkState(remainingWeight >= weight); + float widthByWeight = weight > 0 ? remainingWidthByWeight * weight / remainingWeight : 0; + remainingWidthByWeight -= widthByWeight; + remainingWeight -= weight; + int x = Math.round(exactX); + Key key = + attributes.buildKey(x, y, Math.round(exactX + widthByWeight + attributes.width) - x); + keyList.add(key); + exactX += widthByWeight + attributes.width; + } + Preconditions.checkState(remainingWeight == 0); + return keyList; } /** @@ -494,10 +735,10 @@ private Row parseRow( TypedArray attributes = resources.obtainAttributes(parser, ROW_ATTRIBUTES); try { verticalGap = getDimensionOrFraction( - attributes.peekValue(ROW_VERTICAL_GAP_INDEX), + Optional.fromNullable(attributes.peekValue(ROW_VERTICAL_GAP_INDEX)), keyboardHeight, defaultKeyAttributes.verticalGap, metrics); rowHeight = getDimensionOrFraction( - attributes.peekValue(ROW_KEY_HEIGHT_INDEX), keyboardHeight, + Optional.fromNullable(attributes.peekValue(ROW_KEY_HEIGHT_INDEX)), keyboardHeight, defaultKeyAttributes.height, metrics); edgeFlags = attributes.getInt(ROW_ROW_EDGE_FLAGS_INDEX, 0); } finally { @@ -505,8 +746,7 @@ private Row parseRow( } } - List keyList = new ArrayList(); - int x = 0; + List keyAttributesList = new ArrayList(16); // 16 is heuristic while (true) { parser.next(); ignoreWhiteSpaceAndComment(parser); @@ -515,25 +755,22 @@ private Row parseRow( break; } if ("Key".equals(parser.getName())) { - Key key = parseKey(x, y, edgeFlags, defaultKeyAttributes, popUpAttributes); - keyList.add(key); - x += key.getWidth(); + keyAttributesList.add(parseKey(edgeFlags, defaultKeyAttributes, popUpAttributes)); } else if ("Spacer".equals(parser.getName())) { - Key key = parseSpacer(x, y, edgeFlags, defaultKeyAttributes); - keyList.add(key); - x += key.getWidth(); + keyAttributesList.add(parseSpacer(edgeFlags, defaultKeyAttributes)); } } assertEndTag(parser, "Row"); + List keyList = buildKeyList(keyAttributesList, y, keyboardWidth); return buildRow(keyList, rowHeight, verticalGap); } /** * Parses a {@code Key} element, and returns an instance. */ - private Key parseKey(int x, int y, int edgeFlags, - KeyAttributes defaultKeyAttributes, PopUpAttributes popUpAttributes) + private KeyAttributes parseKey(int edgeFlags, KeyAttributes defaultKeyAttributes, + PopUpAttributes popUpAttributes) throws XmlPullParserException, IOException { XmlResourceParser parser = this.xmlResourceParser; assertStartTag(parser, "Key"); @@ -541,19 +778,20 @@ private Key parseKey(int x, int y, int edgeFlags, KeyAttributes keyAttributes; boolean isRepeatable; boolean isModifier; - boolean isSticky; + DisplayMetrics metrics = resources.getDisplayMetrics(); { TypedArray attributes = resources.obtainAttributes(parser, KEY_ATTRIBUTES); try { - DisplayMetrics metrics = resources.getDisplayMetrics(); keyAttributes = parseKeyAttributes( attributes, defaultKeyAttributes, metrics, keyboardWidth, keyboardHeight, - KEY_KEY_WIDTH_INDEX, KEY_KEY_HEIGHT_INDEX, KEY_HORIZONTAL_GAP_INDEX, -1, + KEY_KEY_WIDTH_INDEX, KEY_KEY_HEIGHT_INDEX, KEY_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX, + KEY_HORIZONTAL_GAP_INDEX, -1, + KEY_DEFAULT_ICON_WIDTH_INDEX, KEY_DEFAULT_ICON_HEIGHT_INDEX, + KEY_DEFAULT_HORIZONTAL_PADDING_INDEX, KEY_DEFAULT_VERTICAL_PADDING_INDEX, KEY_KEY_BACKGROUND_INDEX); edgeFlags |= attributes.getInt(KEY_KEY_EDGE_FLAGS_INDEX, 0); isRepeatable = attributes.getBoolean(KEY_IS_REPEATABLE_INDEX, false); isModifier = attributes.getBoolean(KEY_IS_MODIFIER_INDEX, false); - isSticky = attributes.getBoolean(KEY_IS_STICKY_INDEX, false); } finally { attributes.recycle(); } @@ -568,18 +806,19 @@ private Key parseKey(int x, int y, int edgeFlags, break; } - keyStateList.add( - parseKeyState(keyAttributes.keyBackgroundDrawableType, popUpAttributes)); + keyStateList.add(parseKeyState(keyAttributes, popUpAttributes, metrics)); } // At the moment, we just accept keys which has default state. boolean hasDefault = false; boolean hasLongPressKeyCode = false; for (KeyState keyState : keyStateList) { - if (keyState.getMetaStateSet().isEmpty()) { + if (keyState.getMetaStateSet().isEmpty() + || keyState.getMetaStateSet().contains(KeyState.MetaState.FALLBACK)) { hasDefault = true; - if (keyState.getFlick(Flick.Direction.CENTER).getKeyEntity().getLongPressKeyCode() != - KeyEntity.INVALID_KEY_CODE) { + Optional flick = keyState.getFlick(Flick.Direction.CENTER); + Preconditions.checkState(flick.isPresent()); + if (flick.get().getKeyEntity().getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE) { hasLongPressKeyCode = true; } break; @@ -589,23 +828,26 @@ private Key parseKey(int x, int y, int edgeFlags, throw new IllegalArgumentException( "No default KeyState element is found: " + parser.getPositionDescription()); } - if (isRepeatable && hasLongPressKeyCode) { throw new IllegalArgumentException( - "The key has both isRepeatable attribute and longPressKeyCode: " + - parser.getPositionDescription()); + "The key has both isRepeatable attribute and longPressKeyCode: " + + parser.getPositionDescription()); } assertEndTag(parser, "Key"); - return new Key( - x, y, keyAttributes.width, keyAttributes.height, keyAttributes.horizontalGap, - edgeFlags, isRepeatable, isModifier, isSticky, Stick.EVEN, keyStateList); + return keyAttributes.toBuilder() + .setEdgeFlags(edgeFlags) + .setRepeatable(isRepeatable) + .setModifier(isModifier) + .setStick(Stick.EVEN) + .setKeyStateList(keyStateList) + .build(); } /** * Parses a {@code Spacer} element, and returns an instance. */ - private Key parseSpacer(int x, int y, int edgeFlags, KeyAttributes defaultKeyAttributes) + private KeyAttributes parseSpacer(int edgeFlags, KeyAttributes defaultKeyAttributes) throws XmlPullParserException, IOException { XmlResourceParser parser = this.xmlResourceParser; DisplayMetrics metrics = resources.getDisplayMetrics(); @@ -618,7 +860,9 @@ private Key parseSpacer(int x, int y, int edgeFlags, KeyAttributes defaultKeyAtt try { keyAttributes = parseKeyAttributes( attributes, defaultKeyAttributes, metrics, keyboardWidth, keyboardHeight, - -1, SPACER_KEY_HEIGHT_INDEX, SPACER_HORIZONTAL_GAP_INDEX, -1, -1); + SPACER_KEY_WIDTH_INDEX, SPACER_KEY_HEIGHT_INDEX, + SPACER_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX, -1, -1, -1, -1, -1, -1, + SPACER_KEY_BACKGROUND_INDEX); edgeFlags |= attributes.getInt(SPACER_KEY_EDGE_FLAGS_INDEX, 0); stick = Stick.values()[attributes.getInt(SPACER_STICK_INDEX, 0)]; } finally { @@ -630,19 +874,21 @@ private Key parseSpacer(int x, int y, int edgeFlags, KeyAttributes defaultKeyAtt assertEndTag(parser, "Spacer"); // Returns a dummy key object. - return new Key( - x, y, keyAttributes.horizontalGap, keyAttributes.height, - 0, edgeFlags, false, false, false, stick, Collections.emptyList()); + return keyAttributes.toBuilder() + .setRepeatable(false) + .setModifier(false) + .setEdgeFlags(edgeFlags) + .setStick(stick) + .build(); } - private KeyState parseKeyState(DrawableType defaultBackgroundDrawableType, - PopUpAttributes popUpAttributes) + private KeyState parseKeyState(KeyAttributes defaultKeyAttributes, + PopUpAttributes popUpAttributes, DisplayMetrics metrics) throws XmlPullParserException, IOException { XmlResourceParser parser = this.xmlResourceParser; assertStartTag(parser, "KeyState"); String contentDescription; - DrawableType backgroundDrawableType; Set metaStateSet; Set nextAddMetaState; Set nextRemoveMetaState; @@ -651,8 +897,6 @@ private KeyState parseKeyState(DrawableType defaultBackgroundDrawableType, try { contentDescription = Objects.firstNonNull( attributes.getText(KEY_STATE_CONTENT_DESCRIPTION_INDEX), "").toString(); - backgroundDrawableType = parseKeyBackgroundDrawableType( - attributes, KEY_STATE_KEY_BACKGROUND_INDEX, defaultBackgroundDrawableType); metaStateSet = parseMetaState(attributes, KEY_STATE_META_STATE_INDEX); nextAddMetaState = parseMetaState(attributes, KEY_STATE_NEXT_META_STATE_INDEX); nextRemoveMetaState = parseMetaState(attributes, KEY_STATE_NEXT_REMOVED_META_STATES_INDEX); @@ -669,7 +913,7 @@ private KeyState parseKeyState(DrawableType defaultBackgroundDrawableType, if (parser.getEventType() == XmlResourceParser.END_TAG) { break; } - flickList.add(parseFlick(backgroundDrawableType, popUpAttributes)); + flickList.add(parseFlick(defaultKeyAttributes, popUpAttributes, metrics)); } // At the moment, we support only keys which has flick data to the CENTER direction. @@ -690,8 +934,8 @@ private KeyState parseKeyState(DrawableType defaultBackgroundDrawableType, flickList); } - private Flick parseFlick(DrawableType backgroundDrawableType, - PopUpAttributes popUpAttributes) + private Flick parseFlick(KeyAttributes defaultKeyAttributes, + PopUpAttributes popUpAttributes, DisplayMetrics metrics) throws XmlPullParserException, IOException { XmlResourceParser parser = this.xmlResourceParser; assertStartTag(parser, "Flick"); @@ -707,13 +951,13 @@ private Flick parseFlick(DrawableType backgroundDrawableType, } parser.next(); - KeyEntity entity = parseKeyEntity(backgroundDrawableType, popUpAttributes); + KeyEntity entity = parseKeyEntity(defaultKeyAttributes, popUpAttributes, metrics); - if (entity.getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE && - direction != Flick.Direction.CENTER) { + if (entity.getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE + && direction != Flick.Direction.CENTER) { throw new IllegalArgumentException( - "longPressKeyCode can be set to only a KenEntity for CENTER direction: " + - parser.getPositionDescription()); + "longPressKeyCode can be set to only a KenEntity for CENTER direction: " + + parser.getPositionDescription()); } parser.next(); @@ -722,8 +966,8 @@ private Flick parseFlick(DrawableType backgroundDrawableType, return new Flick(direction, entity); } - private KeyEntity parseKeyEntity( - DrawableType backgroundDrawableType, PopUpAttributes popUpAttributes) + private KeyEntity parseKeyEntity(KeyAttributes defaultKeyAttributes, + PopUpAttributes popUpAttributes, DisplayMetrics metrics) throws XmlPullParserException, IOException { XmlResourceParser parser = this.xmlResourceParser; assertStartTag(parser, "KeyEntity"); @@ -731,11 +975,14 @@ private KeyEntity parseKeyEntity( int sourceId; int keyCode; int longPressKeyCode; + boolean longPressTimeoutTrigger; int keyIconResourceId; - String keyCharacter; - @SuppressWarnings("unused") - DrawableType keyBackgroundDrawableType; + Optional keyCharacter = Optional.absent(); boolean flickHighlight; + int horizontalPadding; + int verticalPadding; + int iconWidth; + int iconHeight; { TypedArray attributes = resources.obtainAttributes(parser, KEY_ENTITY_ATTRIBUTES); try { @@ -743,15 +990,33 @@ private KeyEntity parseKeyEntity( if (!sourceIdSet.add(sourceId)) { // Same sourceId is found. throw new IllegalArgumentException( - "Duplicataed sourceId is found: " + xmlResourceParser.getPositionDescription()); + "Duplicataed sourceId (" + sourceId + ") is found: " + + xmlResourceParser.getPositionDescription()); } keyCode = getCode( - attributes.peekValue(KEY_ENTITY_KEY_CODE_INDEX), KeyEntity.INVALID_KEY_CODE); - longPressKeyCode = getCode(attributes.peekValue(KEY_ENTITY_LONG_PRESS_KEY_CODE_INDEX), - KeyEntity.INVALID_KEY_CODE); + Optional.fromNullable(attributes.peekValue(KEY_ENTITY_KEY_CODE_INDEX)), + KeyEntity.INVALID_KEY_CODE); + longPressKeyCode = getCode( + Optional.fromNullable(attributes.peekValue(KEY_ENTITY_LONG_PRESS_KEY_CODE_INDEX)), + KeyEntity.INVALID_KEY_CODE); + longPressTimeoutTrigger = attributes.getBoolean(KEY_ENTITY_LONG_PRESS_TIMEOUT_TRIGGER_INDEX, + true); keyIconResourceId = attributes.getResourceId(KEY_ENTITY_KEY_ICON_INDEX, 0); - keyCharacter = attributes.getString(KEY_ENTITY_KEY_CHAR_INDEX); + keyCharacter = Optional.fromNullable(attributes.getString(KEY_ENTITY_KEY_CHAR_INDEX)); flickHighlight = attributes.getBoolean(KEY_ENTITY_FLICK_HIGHLIGHT_INDEX, false); + + horizontalPadding = getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(KEY_ENTITY_HORIZONTAL_PADDING_INDEX)), + keyboardWidth, defaultKeyAttributes.defaultHorizontalPadding, metrics); + verticalPadding = getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(KEY_ENTITY_VERTICAL_PADDING_INDEX)), + keyboardHeight, defaultKeyAttributes.defaultVerticalPadding, metrics); + iconWidth = getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(KEY_ENTITY_ICON_WIDTH_INDEX)), + keyboardWidth, defaultKeyAttributes.defaultIconWidth, metrics); + iconHeight = getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(KEY_ENTITY_ICON_HEIGHT_INDEX)), + keyboardHeight, defaultKeyAttributes.defaultIconHeight, metrics); } finally { attributes.recycle(); } @@ -760,84 +1025,128 @@ private KeyEntity parseKeyEntity( parser.next(); ignoreWhiteSpaceAndComment(parser); - PopUp popUp = null; + Optional popUp = Optional.absent(); if (parser.getEventType() == XmlResourceParser.START_TAG) { - popUp = parsePopUp(popUpAttributes); + popUp = Optional.of(parsePopUp(popUpAttributes)); parser.next(); ignoreWhiteSpaceAndComment(parser); } assertEndTag(parser, "KeyEntity"); - return new KeyEntity(sourceId, keyCode, longPressKeyCode, keyIconResourceId, keyCharacter, - backgroundDrawableType, flickHighlight, popUp); + return new KeyEntity(sourceId, keyCode, longPressKeyCode, longPressTimeoutTrigger, + keyIconResourceId, keyCharacter, flickHighlight, popUp, + horizontalPadding, verticalPadding, iconWidth, iconHeight); } - private PopUp parsePopUp(PopUpAttributes popUpAttributes) + private PopUp parsePopUp(PopUpAttributes defaultValue) throws XmlPullParserException, IOException { XmlResourceParser parser = this.xmlResourceParser; assertStartTag(parser, "PopUp"); + PopUpAttributes popUpAttributes; + TypedArray attributes = resources.obtainAttributes(parser, POPUP_ATTRIBUTES); int popUpIconResourceId; - { - TypedArray attributes = resources.obtainAttributes(parser, R.styleable.PopUp); - try { - popUpIconResourceId = attributes.getResourceId(R.styleable.PopUp_popUpIcon, 0); - } finally { - attributes.recycle(); - } + int popUpLongPressIconResourceId; + try { + popUpAttributes = parsePopUpAttributes(attributes, defaultValue, + resources.getDisplayMetrics(), + keyboardWidth, POPUP_KEY_HEIGHT_INDEX, + POPUP_KEY_X_OFFSET_INDEX, POPUP_KEY_Y_OFFSET_INDEX, + POPUP_KEY_ICON_WIDTH_INDEX, POPUP_KEY_ICON_HEIGHT_INDEX); + popUpIconResourceId = attributes.getResourceId(POPUP_KEY_ICON_INDEX, 0); + popUpLongPressIconResourceId = attributes.getResourceId(POPUP_KEY_LONG_PRESS_ICON_INDEX, 0); + } finally { + attributes.recycle(); } parser.next(); assertEndTag(parser, "PopUp"); return new PopUp(popUpIconResourceId, - popUpAttributes.popUpWidth, + popUpLongPressIconResourceId, popUpAttributes.popUpHeight, popUpAttributes.popUpXOffset, - popUpAttributes.popUpYOffset); + popUpAttributes.popUpYOffset, + popUpAttributes.popUpIconWidth, + popUpAttributes.popUpIconHeight); } private float parseFlickThreshold(TypedArray attributes, int index) { float flickThreshold = attributes.getDimension( index, resources.getDimension(R.dimen.default_flick_threshold)); - if (flickThreshold <= 0) { - throw new IllegalArgumentException( - "flickThreshold must be greater than 0. value = " + flickThreshold); - } + Preconditions.checkArgument( + flickThreshold > 0, "flickThreshold must be greater than 0. value = " + flickThreshold); return flickThreshold; } private static KeyAttributes parseKeyAttributes( TypedArray attributes, KeyAttributes defaultValue, DisplayMetrics metrics, int keyboardWidth, int keyboardHeight, - int keyWidthIndex, int keyHeightIndex, int horizontalGapIndex, int verticalGapIndex, + int keyWidthIndex, int keyHeightIndex, int keyHorizontalLayoutWeightIndex, + int horizontalGapIndex, int verticalGapIndex, + int defaultIconWidthIndex, int defaultIconHeightIndex, + int defaultHorizontalPaddingIndex, int defaultVerticalPaddingIndex, int keyBackgroundIndex) { + int keyWidth = (keyWidthIndex >= 0) ? getDimensionOrFraction( - attributes.peekValue(keyWidthIndex), keyboardWidth, + Optional.fromNullable(attributes.peekValue(keyWidthIndex)), keyboardWidth, defaultValue.width, metrics) : defaultValue.width; int keyHeight = (keyHeightIndex >= 0) ? getDimensionOrFraction( - attributes.peekValue(keyHeightIndex), keyboardHeight, + Optional.fromNullable(attributes.peekValue(keyHeightIndex)), keyboardHeight, defaultValue.height, metrics) : defaultValue.height; + int keyHorizontalLayoutWeight = (keyHorizontalLayoutWeightIndex >= 0) + ? attributes.getInt(keyHorizontalLayoutWeightIndex, defaultValue.horizontalLayoutWeight) + : defaultValue.horizontalLayoutWeight; int horizontalGap = (horizontalGapIndex >= 0) ? getDimensionOrFraction( - attributes.peekValue(horizontalGapIndex), + Optional.fromNullable(attributes.peekValue(horizontalGapIndex)), keyboardWidth, defaultValue.horizontalGap, metrics) : defaultValue.horizontalGap; int verticalGap = (verticalGapIndex >= 0) ? getDimensionOrFraction( - attributes.peekValue(verticalGapIndex), + Optional.fromNullable(attributes.peekValue(verticalGapIndex)), keyboardHeight, defaultValue.verticalGap, metrics) : defaultValue.verticalGap; + int defaultIconWidth = (defaultIconWidthIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(defaultIconWidthIndex)), + keyboardWidth, defaultValue.defaultIconWidth, metrics) + : defaultValue.defaultIconWidth; + int defaultIconHeight = (defaultIconHeightIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(defaultIconHeightIndex)), + keyboardHeight, defaultValue.defaultIconHeight, metrics) + : defaultValue.defaultIconHeight; + int defaultHorizontalPadding = (defaultHorizontalPaddingIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(defaultHorizontalPaddingIndex)), + keyboardWidth, defaultValue.defaultHorizontalPadding, metrics) + : defaultValue.defaultHorizontalPadding; + int defaultVerticalPadding = (defaultVerticalPaddingIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(defaultVerticalPaddingIndex)), + keyboardWidth, defaultValue.defaultVerticalPadding, metrics) + : defaultValue.defaultVerticalPadding; DrawableType keyBackgroundDrawableType = parseKeyBackgroundDrawableType( attributes, keyBackgroundIndex, defaultValue.keyBackgroundDrawableType); - return new KeyAttributes( - keyWidth, keyHeight, horizontalGap, verticalGap, keyBackgroundDrawableType); + return KeyAttributes.newBuilder() + .setWidth(keyWidth) + .setHeight(keyHeight) + .setHorizontalLayoutWeight(keyHorizontalLayoutWeight) + .setHorizontalGap(horizontalGap) + .setVerticalGap(verticalGap) + .setDefaultHorizontalPadding(defaultHorizontalPadding) + .setDefaultVerticalPadding(defaultVerticalPadding) + .setDefaultIconWidth(defaultIconWidth) + .setDefaultIconHeight(defaultIconHeight) + .setKeybackgroundDrawableType(keyBackgroundDrawableType) + .build(); } private static DrawableType parseKeyBackgroundDrawableType( @@ -850,17 +1159,37 @@ private static DrawableType parseKeyBackgroundDrawableType( } private static PopUpAttributes parsePopUpAttributes( - TypedArray attributes, DisplayMetrics metrics, int keyboardWidth, - int popUpWidthIndex, int popUpHeightIndex, int popUpXOffsetIndex, int popUpYOffsetIndex) { - int popUpWidth = getDimensionOrFraction( - attributes.peekValue(popUpWidthIndex), keyboardWidth, 0, metrics); - int popUpHeight = getDimensionOrFraction( - attributes.peekValue(popUpHeightIndex), keyboardWidth, 0, metrics); - int popUpXOffset = getDimensionOrFraction( - attributes.peekValue(popUpXOffsetIndex), keyboardWidth, 0, metrics); - int popUpYOffset = getDimensionOrFraction( - attributes.peekValue(popUpYOffsetIndex), keyboardWidth, 0, metrics); - return new PopUpAttributes(popUpWidth, popUpHeight, popUpXOffset, popUpYOffset); + TypedArray attributes, PopUpAttributes defaultValue, DisplayMetrics metrics, + int keyboardWidth, int popUpHeightIndex, + int popUpXOffsetIndex, int popUpYOffsetIndex, + int popUpIconWidthIndex, int popUpIconHeightIndex) { + int popUpHeight = (popUpHeightIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(popUpHeightIndex)), keyboardWidth, + defaultValue.popUpHeight, metrics) + : defaultValue.popUpHeight; + int popUpXOffset = (popUpXOffsetIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(popUpXOffsetIndex)), keyboardWidth, + defaultValue.popUpXOffset, metrics) + : defaultValue.popUpXOffset; + int popUpYOffset = (popUpYOffsetIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(popUpYOffsetIndex)), keyboardWidth, + defaultValue.popUpYOffset, metrics) + : defaultValue.popUpYOffset; + int popUpIconWidth = (popUpIconWidthIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(popUpIconWidthIndex)), keyboardWidth, + defaultValue.popUpIconWidth, metrics) + : defaultValue.popUpIconWidth; + int popUpIconHeight = (popUpIconHeightIndex >= 0) + ? getDimensionOrFraction( + Optional.fromNullable(attributes.peekValue(popUpIconHeightIndex)), keyboardWidth, + defaultValue.popUpIconHeight, metrics) + : defaultValue.popUpIconHeight; + return new PopUpAttributes( + popUpHeight, popUpXOffset, popUpYOffset, popUpIconWidth, popUpIconHeight); } /** @@ -889,13 +1218,7 @@ private Flick.Direction parseFlickDirection(TypedArray attributes, int index) { return Flick.Direction.valueOf(attributes.getInt(index, Flick.Direction.CENTER.index)); } - protected Keyboard buildKeyboard( - Optional contentDescription, List rowList, float flickThreshold) { - return new Keyboard(Preconditions.checkNotNull(contentDescription), - Preconditions.checkNotNull(rowList), flickThreshold); - } - - protected Row buildRow(List keyList, int height, int verticalGap) { + private Row buildRow(List keyList, int height, int verticalGap) { return new Row(keyList, height, verticalGap); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java index 6fa2d4c29..ac472048e 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java @@ -37,20 +37,17 @@ import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent; import org.mozc.android.inputmethod.japanese.resources.R; import org.mozc.android.inputmethod.japanese.view.DrawableCache; -import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.collect.ForwardingMap; import com.google.common.collect.Sets; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Shader.TileMode; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.os.Looper; import android.support.v4.view.ViewCompat; import android.text.InputType; @@ -74,31 +71,85 @@ * */ public class KeyboardView extends View implements MemoryManageable { + private final BackgroundDrawableFactory backgroundDrawableFactory = - new BackgroundDrawableFactory(getResources().getDisplayMetrics().density); - private final DrawableCache drawableCache = - new DrawableCache(new MozcDrawableFactory(getResources())); + new BackgroundDrawableFactory(getResources()); + private final DrawableCache drawableCache = new DrawableCache(getResources()); private final PopUpPreview.Pool popupPreviewPool = new PopUpPreview.Pool( this, Looper.getMainLooper(), backgroundDrawableFactory, drawableCache); private final long popupDismissDelay; - private Keyboard keyboard; + private Optional keyboard = Optional.absent(); // Do not update directly. Use setMetaState instead. @VisibleForTesting Set metaState; @VisibleForTesting final KeyboardViewBackgroundSurface backgroundSurface = new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache); @VisibleForTesting boolean isKeyPressed; - private final int keycodeSymbol; private final float scaledDensity; private int flickSensitivity; private boolean popupEnabled = true; - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; private final KeyboardAccessibilityDelegate accessibilityDelegate; + /** + * Decorator class for {@code Map} for {@code KeyEventContextMap}. + *

    + * When the number of the content is changed, meta state "HANDLING_TOUCH_EVENT" + * is updated. + */ + private final class KeyEventContextMap extends ForwardingMap{ + + private final Map delegate; + + private KeyEventContextMap(Map delegate) { + this.delegate = delegate; + } + + @Override + protected Map delegate() { + return delegate; + } + + @Override + public void clear() { + super.clear(); + updateHandlingTouchEventMetaState(); + } + + @Override + public KeyEventContext put(Integer key, KeyEventContext value) { + KeyEventContext result = super.put(key, value); + updateHandlingTouchEventMetaState(); + return result; + } + + @Override + public void putAll(Map map) { + super.putAll(map); + updateHandlingTouchEventMetaState(); + } + + @Override + public KeyEventContext remove(Object object) { + KeyEventContext result = super.remove(object); + updateHandlingTouchEventMetaState(); + return result; + } + + private void updateHandlingTouchEventMetaState() { + if (keyEventContextMap.isEmpty()) { + updateMetaStates(Collections.emptySet(), + Collections.singleton(MetaState.HANDLING_TOUCH_EVENT)); + } else { + updateMetaStates(Collections.singleton(MetaState.HANDLING_TOUCH_EVENT), + Collections.emptySet()); + } + } + } + // A map from pointerId to KeyEventContext. // Note: the pointerId should be small integers, e.g. 0, 1, 2... So if it turned out // that the usage of Map is heavy, we probably can replace this map by a List or @@ -107,9 +158,9 @@ public class KeyboardView extends View implements MemoryManageable { // in the pressing order in flushPendingKeyEvent. // Its initial capacity (16) and load factor (0.75) are just heuristics. @VisibleForTesting public final Map keyEventContextMap = - new LinkedHashMap(16, 0.75f, false); + new KeyEventContextMap(new LinkedHashMap(16, 0.75f, false)); - private KeyEventHandler keyEventHandler = null; + private Optional keyEventHandler = Optional.absent(); // This constructor is package private for this unit test. public KeyboardView(Context context) { @@ -129,11 +180,37 @@ public KeyboardView(Context context, AttributeSet attrs, int defStyle) { Context context = getContext(); Resources res = context.getResources(); popupDismissDelay = res.getInteger(R.integer.config_popup_dismiss_delay); - keycodeSymbol = res.getInteger(R.integer.key_symbol); scaledDensity = res.getDisplayMetrics().scaledDensity; - accessibilityDelegate = new KeyboardAccessibilityDelegate(this); + accessibilityDelegate = new KeyboardAccessibilityDelegate( + this, new KeyboardAccessibilityDelegate.TouchEventEmulator() { + @Override + public void emulateLongPress(Key key) { + Preconditions.checkNotNull(key); + emulateImpl(key, true); + } + + @Override + public void emulateKeyInput(Key key) { + Preconditions.checkNotNull(key); + emulateImpl(key, false); + } + + private void emulateImpl(Key key, boolean isLongPress) { + KeyEventContext keyEventContext = new KeyEventContext(key, 0, 0, 0, 0, 0, 0, metaState); + processKeyEventContextForOnDownEvent(keyEventContext); + if (isLongPress && keyEventHandler.isPresent()) { + keyEventHandler.get().handleMessageLongPress(keyEventContext); + } + processKeyEventContextForOnUpEvent(keyEventContext); + // Without the invalidation this view cannot know that its content + // has been updated. + invalidate(); + } + }); ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate); - setMetaStates(Collections.emptySet()); + // Not sure if globe is really activated. + // However metastate requires GLOBE or NO_GLOBE state. + setMetaStates(EnumSet.of(MetaState.NO_GLOBE)); } /** @@ -153,24 +230,23 @@ private float getFlickSensitivityInDip() { return -flickSensitivity * 1.5f * scaledDensity; } - KeyEventContext getKeyEventContextByKey(Key key) { + private Optional getKeyEventContextByKey(Key key) { + Preconditions.checkNotNull(key); for (KeyEventContext keyEventContext : keyEventContextMap.values()) { - if (keyEventContext != null && key == keyEventContext.key) { - return keyEventContext; + if (key == keyEventContext.key) { + return Optional.of(keyEventContext); } } - return null; + return Optional.absent(); } private void disposeKeyEventContext(KeyEventContext keyEventContext) { - if (keyEventContext == null) { - return; + Preconditions.checkNotNull(keyEventContext); + if (keyEventHandler.isPresent()) { + keyEventHandler.get().cancelDelayedKeyEvent(keyEventContext); } - if (keyEventHandler != null) { - keyEventHandler.cancelDelayedKeyEvent(keyEventContext); - } - backgroundSurface.requestUpdateKey(keyEventContext.key, null); - if (popupEnabled) { + backgroundSurface.requestUpdateKey(keyEventContext.key, Optional.absent()); + if (popupEnabled || keyEventContext.longPressCallback.isPresent()) { popupPreviewPool.releaseDelayed(keyEventContext.pointerId, popupDismissDelay); } } @@ -186,7 +262,9 @@ public void resetState() { keyEventContextMap.clear(); } - private void flushPendingKeyEvent(TouchEvent relativeTouchEvent) { + private void flushPendingKeyEvent(Optional relativeTouchEvent) { + Preconditions.checkNotNull(relativeTouchEvent); + // Back up values and clear the map first to avoid stack overflow // in case this method is invoked recursively from the callback. // TODO(hidehiko): Refactor around keyEventHandler and keyEventContext. Also we should be @@ -194,30 +272,30 @@ private void flushPendingKeyEvent(TouchEvent relativeTouchEvent) { KeyEventContext[] keyEventContextArray = keyEventContextMap.values().toArray(new KeyEventContext[keyEventContextMap.size()]); keyEventContextMap.clear(); - KeyEventHandler keyEventHandler = this.keyEventHandler; for (KeyEventContext keyEventContext : keyEventContextArray) { int keyCode = keyEventContext.getKeyCode(); int pressedKeyCode = keyEventContext.getPressedKeyCode(); disposeKeyEventContext(keyEventContext); - if (keyEventHandler != null) { + if (keyEventHandler.isPresent()) { // Send relativeTouchEvent as well if exists. - List touchEventList = relativeTouchEvent == null - ? Collections.singletonList(keyEventContext.getTouchEvent()) - : Arrays.asList(relativeTouchEvent, keyEventContext.getTouchEvent()); - keyEventHandler.sendKey(keyCode, touchEventList); - keyEventHandler.sendRelease(pressedKeyCode); + // TODO(hsumita): Confirm that we can put null on touchEventList or not. + List touchEventList = relativeTouchEvent.isPresent() + ? Arrays.asList(relativeTouchEvent.get(), keyEventContext.getTouchEvent().orNull()) + : Collections.singletonList(keyEventContext.getTouchEvent().orNull()); + keyEventHandler.get().sendKey(keyCode, touchEventList); + keyEventHandler.get().sendRelease(pressedKeyCode); } } } /** Set a given keyboard to this view, and send a request to update. */ public void setKeyboard(Keyboard keyboard) { - flushPendingKeyEvent(null); + flushPendingKeyEvent(Optional.absent()); - this.keyboard = keyboard; + this.keyboard = Optional.of(keyboard); updateMetaStates(Collections.emptySet(), MetaState.CHAR_TYPE_EXCLUSIVE_GROUP); - accessibilityDelegate.setKeyboard(Optional.fromNullable(keyboard)); + accessibilityDelegate.setKeyboard(this.keyboard); this.drawableCache.clear(); backgroundSurface.requestUpdateKeyboard(keyboard, metaState); backgroundSurface.requestUpdateSize(keyboard.contentRight - keyboard.contentLeft, @@ -244,93 +322,107 @@ public void onDetachedFromWindow() { } /** @return the current keyboard instance */ - public Keyboard getKeyboard() { + public Optional getKeyboard() { return keyboard; } - public void setSkinType(SkinType skinType) { - this.skinType = skinType; - drawableCache.setSkinType(skinType); - backgroundDrawableFactory.setSkinType(skinType); - resetBackground(); - if (keyboard != null) { - backgroundSurface.requestUpdateKeyboard(keyboard, metaState); - } - } - @SuppressWarnings("deprecation") - private void resetBackground() { - Optional optionalKeyboardBackground = - drawableCache.getDrawable(skinType.windowBackgroundResourceId); - if (!optionalKeyboardBackground.isPresent()) { - setBackgroundColor(Color.BLACK); // Set default background color. - } else { - Drawable keyboardBackground = optionalKeyboardBackground.get(); - if (keyboardBackground instanceof BitmapDrawable) { - // If the background is bitmap resource, set repeat mode. - BitmapDrawable.class.cast(keyboardBackground).setTileModeXY( - TileMode.REPEAT, TileMode.REPEAT); - } - setBackgroundDrawable(keyboardBackground); + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + drawableCache.setSkin(skin); + backgroundDrawableFactory.setSkin(skin); + if (keyboard.isPresent()) { + backgroundSurface.requestUpdateKeyboard(keyboard.get(), metaState); } + setBackgroundDrawable(skin.windowBackgroundDrawable.getConstantState().newDrawable()); } public void setKeyEventHandler(KeyEventHandler keyEventHandler) { // This method needs to be invoked from a thread which the looper held by older keyEventHandler // points. Otherwise, there can be inconsistent state. - KeyEventHandler oldKeyEventHandler = this.keyEventHandler; - if (oldKeyEventHandler != null) { + Optional oldKeyEventHandler = this.keyEventHandler; + if (oldKeyEventHandler.isPresent()) { // Cancel pending key event messages sent by this view. for (KeyEventContext keyEventContext : keyEventContextMap.values()) { - oldKeyEventHandler.cancelDelayedKeyEvent(keyEventContext); + oldKeyEventHandler.get().cancelDelayedKeyEvent(keyEventContext); } } - - this.keyEventHandler = keyEventHandler; - accessibilityDelegate.setKeyEventHandler(Optional.fromNullable(keyEventHandler)); + this.keyEventHandler = Optional.of(keyEventHandler); } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); - if (keyboard == null) { + if (!keyboard.isPresent()) { // We have nothing to do. return; } - + // Draw keyboard. backgroundSurface.update(); backgroundSurface.draw(canvas); } + @SuppressLint("InlinedApi") private static int getPointerIndex(int action) { return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; } private void onDown(MotionEvent event) { + Preconditions.checkState(keyboard.isPresent()); + int pointerIndex = getPointerIndex(event.getAction()); float x = event.getX(pointerIndex); float y = event.getY(pointerIndex); - Key key = getKeyByCoord(x, y); - if (key == null) { + Optional optionalKey = getKeyByCoord(x, y); + if (!optionalKey.isPresent()) { // Just ignore if a key isn't found. return; } - if (getKeyEventContextByKey(key) != null) { + if (getKeyEventContextByKey(optionalKey.get()).isPresent()) { // If the key is already pressed, we simply ignore event sequence related to this press. return; } // Create a new key event context. int pointerId = event.getPointerId(pointerIndex); - float flickThreshold = Math.max(keyboard.getFlickThreshold() + getFlickSensitivityInDip(), 1); - KeyEventContext keyEventContext = new KeyEventContext( - key, pointerId, x, y, getWidth(), getHeight(), + float flickThreshold = + Math.max(keyboard.get().getFlickThreshold() + getFlickSensitivityInDip(), 1); + final KeyEventContext keyEventContext = new KeyEventContext( + optionalKey.get(), pointerId, x, y, getWidth(), getHeight(), flickThreshold * flickThreshold, metaState); + // Show popup. + updatePopUp(keyEventContext, false); + Optional keyEntity = + KeyEventContext.getKeyEntity(keyEventContext.key, metaState, + Optional.of(Flick.Direction.CENTER)); + if (keyEntity.isPresent() && keyEntity.get().getPopUp().isPresent() + && !keyEntity.get().isLongPressTimeoutTrigger()) { + keyEventContext.setLongPressCallback(new Runnable() { + @Override + public void run() { + updatePopUp(keyEventContext, true); + } + }); + } + + // Process the KeyEventContext (e.g., sending messages to KeyEventHandler, updating the surface, + // flushing pending key events and so on) + processKeyEventContextForOnDownEvent(keyEventContext); + // keyEventContextMap contains older event. + // TODO(hidehiko): Switch to ignoring new event, or overwriting the old event + // not to show unknown exceptions to users. + Preconditions.checkState( + keyEventContextMap.put(pointerId, keyEventContext) == null, + "Conflicting keyEventContext is found: " + pointerId); + } + + private void processKeyEventContextForOnDownEvent( + final KeyEventContext keyEventContext) { Set nextMetaStates = keyEventContext.getNextMetaStates(metaState); if (!nextMetaStates.equals(metaState)) { @@ -350,41 +442,29 @@ key, pointerId, x, y, getWidth(), getHeight(), // Update the metaState and request to update the full keyboard image // to update all key icons. setMetaStates(nextMetaStates); - backgroundSurface.requestUpdateKeyboard(keyboard, nextMetaStates); + backgroundSurface.requestUpdateKeyboard(keyboard.get(), nextMetaStates); } else { // Remember if a non-modifier key is pressed. isKeyPressed = true; // Request to update the image of only this key on the view. - backgroundSurface.requestUpdateKey(key, keyEventContext.flickDirection); - } - - if (keyEventContextMap.put(pointerId, keyEventContext) != null) { - // keyEventContextMap contains older event. - // TODO(hidehiko): Switch to ignoring new event, or overwriting the old event - // not to show unknown exceptions to users. - throw new IllegalStateException("Conflicting keyEventContext is found: " + pointerId); + backgroundSurface.requestUpdateKey(keyEventContext.key, + Optional.of(keyEventContext.flickDirection)); } - - // Show popup. - if (popupEnabled) { - popupPreviewPool.getInstance(pointerId) - .showIfNecessary(key, keyEventContext.getCurrentPopUp()); - } - - if (keyEventHandler != null) { + if (keyEventHandler.isPresent()) { // Clear pending key events and overwrite by this press key's one. for (KeyEventContext context : keyEventContextMap.values()) { - keyEventHandler.cancelDelayedKeyEvent(context); + keyEventHandler.get().cancelDelayedKeyEvent(context); } - keyEventHandler.maybeStartDelayedKeyEvent(keyEventContext); - + keyEventHandler.get().maybeStartDelayedKeyEvent(keyEventContext); // Finally we send a notification to listeners. - keyEventHandler.sendPress(keyEventContext.getPressedKeyCode()); + keyEventHandler.get().sendPress(keyEventContext.getPressedKeyCode()); } } private void onUp(MotionEvent event) { + Preconditions.checkState(keyboard.isPresent()); + int pointerIndex = getPointerIndex(event.getAction()); KeyEventContext keyEventContext = keyEventContextMap.remove(event.getPointerId(pointerIndex)); if (keyEventContext == null) { @@ -396,21 +476,22 @@ private void onUp(MotionEvent event) { float y = event.getY(pointerIndex); keyEventContext.update(x, y, TouchAction.TOUCH_UP, event.getEventTime() - event.getDownTime()); + processKeyEventContextForOnUpEvent(keyEventContext); + } + + private void processKeyEventContextForOnUpEvent(KeyEventContext keyEventContext) { + disposeKeyEventContext(keyEventContext); + int keyCode = keyEventContext.getKeyCode(); int pressedKeyCode = keyEventContext.getPressedKeyCode(); - disposeKeyEventContext(keyEventContext); - if (keyEventHandler != null) { - // In multi touch event, CursorView and SymbolInputView can't show by not primary touch event - // because user may intend to input characters rapidly by multi touch, not change mode. - // TODO(yoichio): Move this logic to ViewManager. "In theory" this should be done - // in the class. - if (keyCode != KeyEntity.INVALID_KEY_CODE && - (keyCode != keycodeSymbol || event.getAction() == MotionEvent.ACTION_UP)) { - keyEventHandler.sendKey(keyCode, - Collections.singletonList(keyEventContext.getTouchEvent())); + if (keyEventHandler.isPresent()) { + if (keyCode != KeyEntity.INVALID_KEY_CODE) { + // TODO(hsumita): Confirm that we can put null as a touch event or not. + keyEventHandler.get().sendKey(keyCode, + Collections.singletonList(keyEventContext.getTouchEvent().orNull())); } - keyEventHandler.sendRelease(pressedKeyCode); + keyEventHandler.get().sendRelease(pressedKeyCode); } if (keyEventContext.isMetaStateToggleEvent()) { @@ -420,7 +501,7 @@ private void onUp(MotionEvent event) { // reset the keyboard's meta state to unmodified. flushPendingKeyEvent(keyEventContext.getTouchEvent()); updateMetaStates(Collections.emptySet(), MetaState.CHAR_TYPE_EXCLUSIVE_GROUP); - backgroundSurface.requestUpdateKeyboard(keyboard, Collections.emptySet()); + backgroundSurface.requestUpdateKeyboard(keyboard.get(), Collections.emptySet()); } } else { if (!metaState.isEmpty() && keyEventContextMap.isEmpty()) { @@ -434,7 +515,8 @@ private void onUp(MotionEvent event) { } if (!nextMetaState.equals(metaState)) { setMetaStates(nextMetaState); - backgroundSurface.requestUpdateKeyboard(keyboard, Collections.emptySet()); + backgroundSurface.requestUpdateKeyboard( + keyboard.get(), Collections.emptySet()); } } } @@ -451,17 +533,17 @@ private void onMove(MotionEvent event) { Key key = keyEventContext.key; if (keyEventContext.update(event.getX(i), event.getY(i), TouchAction.TOUCH_MOVE, event.getEventTime() - event.getDownTime())) { - // The key's state is updated from, at least, initial state, so we'll cancel the - // pending key events. - if (keyEventHandler != null) { - keyEventHandler.cancelDelayedKeyEvent(keyEventContext); - } - if (popupEnabled) { - popupPreviewPool.getInstance(keyEventContext.pointerId).showIfNecessary( - key, keyEventContext.getCurrentPopUp()); + if (keyEventHandler.isPresent()) { + // The key's state is updated from, at least, initial state, so we'll cancel the + // pending key events, and invoke new pending key events if necessary. + keyEventHandler.get().cancelDelayedKeyEvent(keyEventContext); + if (keyEventContext.flickDirection == Flick.Direction.CENTER) { + keyEventHandler.get().maybeStartDelayedKeyEvent(keyEventContext); + } } + updatePopUp(keyEventContext, false); } - backgroundSurface.requestUpdateKey(key, keyEventContext.flickDirection); + backgroundSurface.requestUpdateKey(key, Optional.of(keyEventContext.flickDirection)); } } @@ -469,8 +551,8 @@ private void onMove(MotionEvent event) { // other onXXX methods defined above. private void onCancel(@SuppressWarnings("unused") MotionEvent event) { resetState(); - if (keyEventHandler != null) { - keyEventHandler.sendCancel(); + if (keyEventHandler.isPresent()) { + keyEventHandler.get().sendCancel(); } } @@ -505,30 +587,31 @@ public boolean onTouchEvent(MotionEvent event) { * Finds a key containing the given coordinate. * @param x {@code x}-coordinate. * @param y {@code y}-coordinate. - * @return A corresponding {@code Key} instance, or {@code null} if not found. + * @return A corresponding {@code Key} instance, or {@code Optional.absent()} if not found. */ - @VisibleForTesting Key getKeyByCoord(float x, float y) { - if (y < 0 || keyboard == null || keyboard.getRowList().isEmpty()) { - return null; + @VisibleForTesting Optional getKeyByCoord(float x, float y) { + if (y < 0 || !keyboard.isPresent() || keyboard.get().getRowList().isEmpty()) { + return Optional.absent(); } + List rowList = keyboard.get().getRowList(); int rowBottom = 0; - Row lastRow = keyboard.getRowList().get(keyboard.getRowList().size() - 1); - for (Row row : keyboard.getRowList()) { + Row lastRow = rowList.get(rowList.size() - 1); + for (Row row : rowList) { rowBottom += row.getHeight() + row.getVerticalGap(); Key prevKey = null; for (Key key : row.getKeyList()) { if ((// Stick vertical gaps to the keys above. - y < key.getY() + key.getHeight() + row.getVerticalGap() || + y < key.getY() + key.getHeight() + row.getVerticalGap() // Or the key is at the bottom of the keyboard. // Note: Some devices sense touch events of out-side of screen. // So, for better user experiences, we return the bottom row // if a user touches below the screen bottom boundary. - row == lastRow || - key.getY() + key.getHeight() >= keyboard.contentBottom) && + || row == lastRow + || key.getY() + key.getHeight() >= keyboard.get().contentBottom) // Horizontal gap is included in the width, // so we don't need to calculate horizontal gap in addition to width. - x < key.getX() + key.getWidth() && + && x < key.getX() + key.getWidth() // The following condition selects a key hit in A, C, or D // (C and D are on the same key), and excludes a key hit in B. // +---+---+ @@ -539,21 +622,21 @@ public boolean onTouchEvent(MotionEvent event) { // The condition y < rowBottom allows hits on A and C, and the other // condition key.getX() <= x allows hits on C and D but not B. // Hence, the hits on B are excluded. - (y < rowBottom || key.getX() <= x)) { + && (y < rowBottom || key.getX() <= x)) { if (!key.isSpacer()) { - return key; // Found a key. + return Optional.of(key); // Found a key. } switch (key.getStick()) { case LEFT: if (prevKey != null) { - return prevKey; + return Optional.of(prevKey); } break; case EVEN: // Split the spacer evenly, assuming we don't have any consecutive spacers. if (x < key.getX() + key.getWidth() / 2 && prevKey != null) { - return prevKey; + return Optional.of(prevKey); } break; case RIGHT: @@ -567,11 +650,11 @@ public boolean onTouchEvent(MotionEvent event) { } if ((y < rowBottom || row == lastRow) && prevKey != null) { - return prevKey; + return Optional.of(prevKey); } } - return null; // Not found. + return Optional.absent(); // Not found. } @Override @@ -583,6 +666,7 @@ public void trimMemory() { @Override public boolean dispatchTouchEvent(MotionEvent event) { + Preconditions.checkNotNull(event); if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { return accessibilityDelegate.dispatchTouchEvent(event); } @@ -591,6 +675,7 @@ public boolean dispatchTouchEvent(MotionEvent event) { @Override public boolean dispatchHoverEvent(MotionEvent event) { + Preconditions.checkNotNull(event); if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { return accessibilityDelegate.dispatchHoverEvent(event); } @@ -598,7 +683,7 @@ public boolean dispatchHoverEvent(MotionEvent event) { } @VisibleForTesting - Set getMetaStates() { + public Set getMetaStates() { return this.metaState; } @@ -607,6 +692,7 @@ private void setMetaStates(Set metaState) { Preconditions.checkArgument(MetaState.isValidSet(metaState)); this.metaState = metaState; accessibilityDelegate.setMetaState(metaState); + backgroundSurface.requestMetaState(metaState); } public void updateMetaStates(Set addedMetaStates, Set removedMetaStates) { @@ -615,6 +701,7 @@ public void updateMetaStates(Set addedMetaStates, Set remo setMetaStates(Sets.union(Sets.difference(metaState, removedMetaStates), addedMetaStates).immutableCopy()); + invalidate(); } public void setPasswordField(boolean isPasswordField) { @@ -625,31 +712,35 @@ public void setEditorInfo(EditorInfo editorInfo) { Preconditions.checkNotNull(editorInfo); Set metaStates = EnumSet.noneOf(MetaState.class); - switch (editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION) { - case EditorInfo.IME_ACTION_DONE: - metaStates.add(MetaState.ACTION_DONE); - break; - case EditorInfo.IME_ACTION_GO: - metaStates.add(MetaState.ACTION_GO); - break; - case EditorInfo.IME_ACTION_NEXT: - metaStates.add(MetaState.ACTION_NEXT); - break; - case EditorInfo.IME_ACTION_NONE: - metaStates.add(MetaState.ACTION_NONE); - break; - case EditorInfo.IME_ACTION_PREVIOUS: - metaStates.add(MetaState.ACTION_PREVIOUS); - break; - case EditorInfo.IME_ACTION_SEARCH: - metaStates.add(MetaState.ACTION_SEARCH); - break; - case EditorInfo.IME_ACTION_SEND: - metaStates.add(MetaState.ACTION_SEND); - break; - default: - // Do nothing + // If IME_FLAG_NO_ENTER_ACTION is set, normal action icon should be shown. + if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) { + switch (editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION) { + case EditorInfo.IME_ACTION_DONE: + metaStates.add(MetaState.ACTION_DONE); + break; + case EditorInfo.IME_ACTION_GO: + metaStates.add(MetaState.ACTION_GO); + break; + case EditorInfo.IME_ACTION_NEXT: + metaStates.add(MetaState.ACTION_NEXT); + break; + case EditorInfo.IME_ACTION_NONE: + metaStates.add(MetaState.ACTION_NONE); + break; + case EditorInfo.IME_ACTION_PREVIOUS: + metaStates.add(MetaState.ACTION_PREVIOUS); + break; + case EditorInfo.IME_ACTION_SEARCH: + metaStates.add(MetaState.ACTION_SEARCH); + break; + case EditorInfo.IME_ACTION_SEND: + metaStates.add(MetaState.ACTION_SEND); + break; + default: + // Do nothing + } } + // InputType variation is *NOT* bit-fields in fact. int clazz = editorInfo.inputType & InputType.TYPE_MASK_CLASS; int variation = editorInfo.inputType & InputType.TYPE_MASK_VARIATION; @@ -674,4 +765,25 @@ public void setEditorInfo(EditorInfo editorInfo) { updateMetaStates(metaStates, Sets.union(MetaState.ACTION_EXCLUSIVE_GROUP, MetaState.VARIATION_EXCLUSIVE_GROUP)); } + + public void setGlobeButtonEnabled(boolean isGlobeButtonEnabled) { + if (isGlobeButtonEnabled) { + updateMetaStates(EnumSet.of(MetaState.GLOBE), EnumSet.of(MetaState.NO_GLOBE)); + } else { + updateMetaStates(EnumSet.of(MetaState.NO_GLOBE), EnumSet.of(MetaState.GLOBE)); + } + } + + private void updatePopUp(KeyEventContext keyEventContext, boolean isDelayedPopUp) { + PopUpPreview popUpPreview = popupPreviewPool.getInstance(keyEventContext.pointerId); + // Even if popup is disabled by preference, delayed popup (== popup for long-press) + // is shown otherwise a user cannot know how long (s)he has to press the key + // to get a character corresponding to long-press. + if (popupEnabled || isDelayedPopUp) { + popUpPreview.showIfNecessary( + keyEventContext.key, keyEventContext.getCurrentPopUp(), isDelayedPopUp); + } else { + popUpPreview.dismiss(); + } + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardViewBackgroundSurface.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardViewBackgroundSurface.java index e1d10d66e..8cd00230e 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardViewBackgroundSurface.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardViewBackgroundSurface.java @@ -32,8 +32,11 @@ import org.mozc.android.inputmethod.japanese.MemoryManageable; import org.mozc.android.inputmethod.japanese.MozcUtil; import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; +import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction; import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; import org.mozc.android.inputmethod.japanese.view.DrawableCache; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import android.graphics.Bitmap; @@ -49,6 +52,8 @@ import java.util.Map; import java.util.Set; +import javax.annotation.Nullable; + /** * Implementation of the background surface for {@link KeyboardView}. * This class takes care of double-buffering and diff-only-updating to improve the rendering @@ -73,7 +78,7 @@ * backgroundSurface.requestUpdateKey(key3, Flick.Direction.RIGHT); * * // Set flick direction to null means the key is released. - * backgroundSurface.requestUpdateKey(key1, null); + * backgroundSurface.requestUpdateKey(key1, Optional.absent()); * * // Actual update is done below. * backgroundSurface.update(); @@ -85,12 +90,12 @@ * This class is exposed as public in order to mock for testing purpose. * */ -public class KeyboardViewBackgroundSurface implements MemoryManageable { +@VisibleForTesting public class KeyboardViewBackgroundSurface implements MemoryManageable { /** * A simple rendering related utilities for keyboard rendering. */ - interface SurfaceCanvas { + @VisibleForTesting interface SurfaceCanvas { /** * Clears a rectangle region of @@ -123,12 +128,11 @@ private static class SurfaceCanvasImpl implements SurfaceCanvas { private final Canvas canvas; SurfaceCanvasImpl(Canvas canvas) { - this.canvas = canvas; + this.canvas = Preconditions.checkNotNull(canvas); } @Override public void clearRegion(int x, int y, int width, int height) { - Canvas canvas = this.canvas; int saveCount = canvas.save(); try { canvas.clipRect(x, y, x + width, y + height, Op.REPLACE); @@ -139,7 +143,7 @@ public void clearRegion(int x, int y, int width, int height) { } @Override - public void drawDrawable(Drawable drawable, int x, int y, int width, int height) { + public void drawDrawable(@Nullable Drawable drawable, int x, int y, int width, int height) { if (drawable == null) { return; } @@ -149,7 +153,7 @@ public void drawDrawable(Drawable drawable, int x, int y, int width, int height) @Override public void drawDrawableAtCenterWithKeepAspectRatio( - Drawable drawable, int x, int y, int width, int height) { + @Nullable Drawable drawable, int x, int y, int width, int height) { if (drawable == null) { return; } @@ -164,8 +168,6 @@ public void drawDrawableAtCenterWithKeepAspectRatio( private void drawDrawableInternal(Drawable drawable, int x, int y, int width, int height, float scale) { - Canvas canvas = this.canvas; - int saveCount = canvas.save(); try { canvas.translate(x, y); @@ -205,24 +207,23 @@ private void drawDrawableInternal(Drawable drawable, int x, int y, int width, in private final BackgroundDrawableFactory backgroundDrawableFactory; private final DrawableCache drawableCache; - public KeyboardViewBackgroundSurface( - BackgroundDrawableFactory backgroundDrawableFactory, DrawableCache drawableCache) { - this.backgroundDrawableFactory = backgroundDrawableFactory; - this.drawableCache = drawableCache; - } - // The current image and its rendering object. - // surfaceCanvas is package private for testing purpose. - private Bitmap surfaceBitmap; - SurfaceCanvas surfaceCanvas; + private Optional surfaceBitmap = Optional.absent(); + @VisibleForTesting Optional surfaceCanvas = Optional.absent(); // Width and height this background surface is to be in pixels private int requestedWidth; private int requestedHeight; private boolean fullUpdateRequested; - private Keyboard requestedKeyboard; - private Set requestedMetaState; + private Optional requestedKeyboard = Optional.absent(); + private Set requestedMetaState = Collections.emptySet(); + + public KeyboardViewBackgroundSurface( + BackgroundDrawableFactory backgroundDrawableFactory, DrawableCache drawableCache) { + this.backgroundDrawableFactory = Preconditions.checkNotNull(backgroundDrawableFactory); + this.drawableCache = Preconditions.checkNotNull(drawableCache); + } /** * A set of pending keys to be updated. @@ -233,29 +234,30 @@ public KeyboardViewBackgroundSurface( * TODO(hidehiko): We should have direction state in somewhere else, not in the pending requests, * so that we can re-render the correct image even if we have any sequence of requests. */ - private final Map pendingKeys = new HashMap(); + @VisibleForTesting + final Map> pendingKeys = + new HashMap>(); /** * Resets and release the current image this instance holds. * By this method's invocation, we assume all key's directions are also reset. */ public void reset() { - if (surfaceBitmap != null) { - surfaceBitmap.recycle(); - surfaceBitmap = null; + if (surfaceBitmap.isPresent()) { + surfaceBitmap.get().recycle(); + surfaceBitmap = Optional.absent(); } - surfaceCanvas = null; + surfaceCanvas = Optional.absent(); pendingKeys.clear(); } /** * Adds the given {@code key} to the pending key set to render it lazily. * In other words, the key isn't rendered until {@link #update()} is invoked. - * If {@code key} is {@code null}, it will be just ignored. */ - public void requestUpdateKey(Key key, Flick.Direction flickDirection) { - if ((key != null) && !key.isSpacer()) { - pendingKeys.put(key, flickDirection); + public void requestUpdateKey(Key key, Optional flickDirection) { + if (!Preconditions.checkNotNull(key).isSpacer()) { + pendingKeys.put(key, Preconditions.checkNotNull(flickDirection)); } } @@ -273,7 +275,7 @@ public void requestUpdateSize(int width, int height) { * This also cancels all key's direction for now. */ public void requestUpdateKeyboard(Keyboard keyboard, Set metaState) { - requestedKeyboard = Preconditions.checkNotNull(keyboard); + requestedKeyboard = Optional.of(keyboard); requestedMetaState = Preconditions.checkNotNull(metaState); fullUpdateRequested = true; @@ -282,6 +284,41 @@ public void requestUpdateKeyboard(Keyboard keyboard, Set metaState) { pendingKeys.clear(); } + /** + * Requests new {@code metaState} to be rendered on this surface. + * + * This method requests redraw only the keys which are required to be redrawn according to + * metastate's change. + * This also cancels such keys' direction for now. + */ + public void requestMetaState(Set newMetaState) { + Preconditions.checkNotNull(newMetaState); + + Set previousMetaState = requestedMetaState; + requestedMetaState = newMetaState; + + if (newMetaState.equals(previousMetaState) || !requestedKeyboard.isPresent()) { + return; + } + + // Update only the keys which should update corresponding KeyState based on given metaState. + for (Row row : requestedKeyboard.get().getRowList()) { + for (Key key : row.getKeyList()) { + KeyState previousKeyState = key.getKeyState(previousMetaState).orNull(); + KeyState newKeyState = key.getKeyState(newMetaState).orNull(); + // Intentionally using != operator instead of equals method. + // - Faster than full-spec equals method. + // - The values of Optional which are returned by Key#getKeyState are always + // the same object so equals is overkill. + if (previousKeyState != newKeyState) { + // Request to draw the key. + // pendingKeys may have already contained corresponding key but overwrite here. + pendingKeys.put(key, Optional.absent()); + } + } + } + } + /** * Actually updates the image this instance holds based on pending update requests. */ @@ -292,7 +329,7 @@ public void update() { fullUpdateRequested = true; } - if (requestedKeyboard == null) { + if (!requestedKeyboard.isPresent()) { // We have nothing to do. return; } @@ -311,25 +348,26 @@ public void update() { } /** - * Returns {@code true} iff (re-)initialization is needed. This method is package private - * just for testing purpose. + * Returns {@code true} iff (re-)initialization is needed. */ - boolean isInitializationNeeded() { + @VisibleForTesting boolean isInitializationNeeded() { // We need to re-create background buffer if // - no initialization is done (after construction or reset). // - the size of view has been changed. - Bitmap bitmap = this.surfaceBitmap; - return (bitmap == null) - || (bitmap.getWidth() != requestedWidth) - || (bitmap.getHeight() != requestedHeight); + Optional bitmap = surfaceBitmap; + return (!bitmap.isPresent()) + || (bitmap.get().getWidth() != requestedWidth) + || (bitmap.get().getHeight() != requestedHeight); } void initialize() { - if (surfaceBitmap != null) { - surfaceBitmap.recycle(); + if (surfaceBitmap.isPresent()) { + surfaceBitmap.get().recycle(); } - surfaceBitmap = MozcUtil.createBitmap(requestedWidth, requestedHeight, Bitmap.Config.ARGB_8888); - surfaceCanvas = new SurfaceCanvasImpl(new Canvas(surfaceBitmap)); + surfaceBitmap = Optional.of( + MozcUtil.createBitmap(requestedWidth, requestedHeight, Bitmap.Config.ARGB_8888)); + surfaceCanvas = Optional.of( + new SurfaceCanvasImpl(new Canvas(surfaceBitmap.get()))); } /** @@ -337,57 +375,84 @@ void initialize() { * It is required to invoke update() method before this method's invocation. */ public void draw(Canvas canvas) { - canvas.drawBitmap(surfaceBitmap, requestedKeyboard.contentLeft, requestedKeyboard.contentTop, - null); + canvas.drawBitmap( + surfaceBitmap.orNull(), requestedKeyboard.get().contentLeft, + requestedKeyboard.get().contentTop, null); } private void clearCanvas() { - Bitmap bitmap = this.surfaceBitmap; - surfaceCanvas.clearRegion(0, 0, bitmap.getWidth(), bitmap.getHeight()); + if (surfaceBitmap.isPresent() && surfaceCanvas.isPresent()) { + Bitmap bitmap = surfaceBitmap.get(); + surfaceCanvas.get().clearRegion(0, 0, bitmap.getWidth(), bitmap.getHeight()); + } } - private void renderKey(Key key, Flick.Direction flickDirection) { + private void renderKey(Key key, Optional flickDirection) { + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(flickDirection); + Preconditions.checkState(surfaceCanvas.isPresent()); + Preconditions.checkState(requestedKeyboard.isPresent()); + SurfaceCanvas canvas = surfaceCanvas.get(); + int horizontalGap = key.getHorizontalGap(); int leftGap = horizontalGap / 2; - boolean isPressed = flickDirection != null; + boolean isPressed = flickDirection.isPresent(); // We split the gap to both sides evenly. - int x = key.getX() + leftGap - requestedKeyboard.contentLeft; - int y = key.getY() - requestedKeyboard.contentTop; - int width = key.getWidth() - horizontalGap; - int height = key.getHeight(); - - KeyEntity keyEntity = getKeyEntityForRendering(key, requestedMetaState, flickDirection); - surfaceCanvas.drawDrawable(getKeyBackground(keyEntity, isPressed), x, y, width, height); - if (keyEntity != null && keyEntity.isFlickHighlightEnabled() && - KeyEventContext.getKeyEntity(key, requestedMetaState, flickDirection) == keyEntity) { - surfaceCanvas.drawDrawable( - backgroundDrawableFactory.getDrawable(FLICK_DRAWABLE_TYPE_MAP.get(flickDirection)), - x, y, width, height); + int x = key.getX() + leftGap - requestedKeyboard.get().contentLeft; + int y = key.getY() - requestedKeyboard.get().contentTop; + // Given width/height for the key. + // The icon is drawn inside the width/height. + int givenWidth = key.getWidth() - horizontalGap; + int givenHeight = key.getHeight(); + + canvas.drawDrawable(getKeyBackground(key, isPressed).orNull(), x, y, givenWidth, givenHeight); + Optional keyEntity = + getKeyEntityForRendering(key, requestedMetaState, flickDirection); + if (flickDirection.isPresent() && keyEntity.isPresent() + && keyEntity.get().isFlickHighlightEnabled() + && KeyEventContext.getKeyEntity(key, requestedMetaState, flickDirection) + .equals(keyEntity)) { + DrawableType drawableType = FLICK_DRAWABLE_TYPE_MAP.get(flickDirection.get()); + Drawable backgroundDrawable = (drawableType != null) + ? backgroundDrawableFactory.getDrawable(drawableType) : null; + canvas.drawDrawable(backgroundDrawable, x, y, givenWidth, givenHeight); } - surfaceCanvas.drawDrawableAtCenterWithKeepAspectRatio( - getKeyIcon(drawableCache, keyEntity, isPressed), x, y, width, height); + int horizontalPadding = keyEntity.isPresent() ? keyEntity.get().getHorizontalPadding() : 0; + int verticalPadding = keyEntity.isPresent() ? keyEntity.get().getVerticalPadding() : 0; + int iconWidth = keyEntity.isPresent() ? keyEntity.get().getIconWidth() : givenWidth; + int iconHeight = keyEntity.isPresent() ? keyEntity.get().getIconHeight() : givenHeight; + iconWidth = Math.min(iconWidth, givenWidth - horizontalPadding * 2); + iconHeight = Math.min(iconHeight, givenHeight - verticalPadding * 2); + canvas.drawDrawableAtCenterWithKeepAspectRatio( + getKeyIcon(drawableCache, keyEntity, isPressed).orNull(), + x + (givenWidth - iconWidth) / 2, y + (givenHeight - iconHeight) / 2, + iconWidth, iconHeight); } /** * Draws all keys in the keyboard. */ private void renderKeyboard() { - for (Row row : requestedKeyboard.getRowList()) { + for (Row row : requestedKeyboard.get().getRowList()) { for (Key key : row.getKeyList()) { - if (!key.isSpacer()) { - renderKey(key, pendingKeys.get(key)); + Optional direction = pendingKeys.get(key); + if (direction == null) { + renderKey(key, Optional.absent()); + } else { + renderKey(key, direction); } } } } private void renderPendingKeys() { + Preconditions.checkState(surfaceCanvas.isPresent()); // The canvas object is used many times, so cache it on local stack. - SurfaceCanvas canvas = this.surfaceCanvas; - int offsetX = requestedKeyboard.contentLeft; - int offsetY = requestedKeyboard.contentTop; - for (Map.Entry entry : pendingKeys.entrySet()) { + SurfaceCanvas canvas = surfaceCanvas.get(); + int offsetX = requestedKeyboard.get().contentLeft; + int offsetY = requestedKeyboard.get().contentTop; + for (Map.Entry> entry : pendingKeys.entrySet()) { Key key = entry.getKey(); // Clear the key's region and then draw the key there. @@ -399,55 +464,52 @@ private void renderPendingKeys() { /** * Returns KeyEntity which should be used for the {@code key}'s rendering with the given state. - * {@code null} will be returned if we don't need to render the key. - * This is package private for testing purpose. + * {@code Optional.absent()} will be returned if we don't need to render the key. */ - static KeyEntity getKeyEntityForRendering( - Key key, Set metaState, Flick.Direction flickDirection) { - if (flickDirection != null) { + @VisibleForTesting static Optional getKeyEntityForRendering( + Key key, Set metaState, Optional flickDirection) { + if (flickDirection.isPresent()) { // If the key is under flick state, check if there is corresponding key entity for the // direction first. - KeyEntity keyEntity = KeyEventContext.getKeyEntity(key, metaState, flickDirection); - if (keyEntity != null) { + Optional keyEntity = KeyEventContext.getKeyEntity(key, metaState, flickDirection); + if (keyEntity.isPresent()) { return keyEntity; } } // Use CENTER direction for released key, or a key flicked to a unsupported direction. - return KeyEventContext.getKeyEntity(key, metaState, Flick.Direction.CENTER); + return KeyEventContext.getKeyEntity(key, metaState, Optional.of(Flick.Direction.CENTER)); } - private static Drawable setDrawableState(Drawable drawable, boolean isPressed) { - if (drawable != null) { - drawable.setState(isPressed ? STATE_PRESSED : STATE_DEFAULT); + private static Optional setDrawableState( + Optional drawable, boolean isPressed) { + if (drawable.isPresent()) { + drawable.get().setState(isPressed ? STATE_PRESSED : STATE_DEFAULT); } return drawable; } /** * Returns {@code Drawable} for the given key's background with setting appropriate state. - * This method is package private just for testing purpose. */ - Drawable getKeyBackground(KeyEntity keyEntity, boolean isPressed) { - if (keyEntity == null) { - return null; - } + @VisibleForTesting Optional getKeyBackground(Key key, boolean isPressed) { + Preconditions.checkNotNull(key); return setDrawableState( - backgroundDrawableFactory.getDrawable(keyEntity.getKeyBackgroundDrawableType()), + Optional.of(backgroundDrawableFactory.getDrawable(key.getKeyBackgroundDrawableType())), isPressed); } /** * Returns {@code Drawable} for the given key's icon with setting appropriate state. - * This method is package private just for testing purpose. */ - static Drawable getKeyIcon( - DrawableCache drawableCache, KeyEntity keyEntity, boolean isPressed) { - if (keyEntity == null) { - return null; + @VisibleForTesting static Optional getKeyIcon( + DrawableCache drawableCache, Optional keyEntity, boolean isPressed) { + if (!keyEntity.isPresent()) { + return Optional.absent(); } + return setDrawableState( - drawableCache.getDrawable(keyEntity.getKeyIconResourceId()).orNull(), isPressed); + drawableCache.getDrawable(keyEntity.get().getKeyIconResourceId()), isPressed); } @Override diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUp.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUp.java index ffc11be8b..81527ccb9 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUp.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUp.java @@ -29,29 +29,36 @@ package org.mozc.android.inputmethod.japanese.keyboard; + /** */ public class PopUp { + private final int popUpIconResourceId; - private final int width; + private final int longPressPopUpIconResourceId; private final int height; private final int xOffset; private final int yOffset; + private final int iconWidth; + private final int iconHeight; - public PopUp(int popUpIconResourceId, int width, int height, int xOffset, int yOffset) { + public PopUp(int popUpIconResourceId, int popUpLongPressIconResourceId, + int height, int xOffset, int yOffset, int iconWidth, int iconHeight) { this.popUpIconResourceId = popUpIconResourceId; - this.width = width; + this.longPressPopUpIconResourceId = popUpLongPressIconResourceId; this.height = height; this.xOffset = xOffset; this.yOffset = yOffset; + this.iconWidth = iconWidth; + this.iconHeight = iconHeight; } public int getPopUpIconResourceId() { return popUpIconResourceId; } - public int getWidth() { - return width; + public int getPopUpLongPressIconResourceId() { + return longPressPopUpIconResourceId; } public int getHeight() { @@ -65,4 +72,12 @@ public int getXOffset() { public int getYOffset() { return yOffset; } + + public int getIconWidth() { + return iconWidth; + } + + public int getIconHeight() { + return iconHeight; + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUpPreview.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUpPreview.java index a14ca7910..727ce16a2 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUpPreview.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/PopUpPreview.java @@ -30,18 +30,21 @@ package org.mozc.android.inputmethod.japanese.keyboard; import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType; +import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.ui.PopUpLayouter; import org.mozc.android.inputmethod.japanese.view.DrawableCache; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.SparseArray; -import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.MarginLayoutParams; -import android.widget.FrameLayout; import android.widget.ImageView; import java.util.ArrayList; @@ -57,7 +60,7 @@ * Production clients shouldn't use this class from outside of this package. * */ -public class PopUpPreview { +@VisibleForTesting public class PopUpPreview { /** * For performance reason, we want to reuse instances of {@link PopUpPreview}. @@ -68,6 +71,7 @@ public class PopUpPreview { * */ static class Pool { + // Typically 2 or 3 popups are shown in maximum. private final SparseArray pool = new SparseArray(3); private final List freeList = new ArrayList(); @@ -78,13 +82,13 @@ static class Pool { Pool(View parent, Looper looper, BackgroundDrawableFactory backgroundDrawableFactory, DrawableCache drawableCache) { - this.parent = parent; - this.backgroundDrawableFactory = backgroundDrawableFactory; - this.drawableCache = drawableCache; - this.dismissHandler = new Handler(looper, new Handler.Callback() { + this.parent = Preconditions.checkNotNull(parent); + this.backgroundDrawableFactory = Preconditions.checkNotNull(backgroundDrawableFactory); + this.drawableCache = Preconditions.checkNotNull(drawableCache); + this.dismissHandler = new Handler(Preconditions.checkNotNull(looper), new Handler.Callback() { @Override public boolean handleMessage(Message message) { - PopUpPreview preview = PopUpPreview.class.cast(message.obj); + PopUpPreview preview = PopUpPreview.class.cast(Preconditions.checkNotNull(message).obj); preview.dismiss(); freeList.add(preview); return true; @@ -133,96 +137,81 @@ void releaseAll() { } } - private static final float POPUP_VIEW_PADDING = 12f; - - private final int padding; - private final View parent; private final BackgroundDrawableFactory backgroundDrawableFactory; private final DrawableCache drawableCache; - @VisibleForTesting final ImageView popupView; + @VisibleForTesting final PopUpLayouter popUp; protected PopUpPreview( View parent, BackgroundDrawableFactory backgroundDrawableFactory, DrawableCache drawableCache) { - this.parent = parent; - this.backgroundDrawableFactory = backgroundDrawableFactory; - this.drawableCache = drawableCache; - this.popupView = new ImageView(parent.getContext()); - popupView.setVisibility(View.GONE); - View rootView = parent.getRootView(); - if (rootView != null) { - FrameLayout screenContent = - FrameLayout.class.cast(rootView.findViewById(android.R.id.content)); - if (screenContent != null) { - screenContent.addView( - popupView, new FrameLayout.LayoutParams(0, 0, Gravity.LEFT | Gravity.TOP)); - } - } - padding = (int) (popupView.getResources().getDisplayMetrics().density * POPUP_VIEW_PADDING); + this.backgroundDrawableFactory = Preconditions.checkNotNull(backgroundDrawableFactory); + this.drawableCache = Preconditions.checkNotNull(drawableCache); + ImageView popUpView = new ImageView(Preconditions.checkNotNull(parent).getContext()); + // To use Canvas#drawPicture(), the view shouldn't be h/w accelerated. + popUpView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + popUpView.setVisibility(View.GONE); + this.popUp = new PopUpLayouter(parent, popUpView); } /** - * Shows the popup preview of the given {@code key} and {@code popup} if needed. + * Shows the pop-up preview of the given {@code key} and {@code optionalPopup} if needed. */ + @SuppressLint("NewApi") @SuppressWarnings("deprecation") - protected void showIfNecessary(Key key, PopUp popup) { - if (key == null || popup == null) { - // No images to be rendered. + protected void showIfNecessary(Key key, Optional optionalPopup, boolean isDelayedPopup) { + Preconditions.checkNotNull(key); + if (!Preconditions.checkNotNull(optionalPopup).isPresent()) { + hidePopupView(); + return; + } + PopUp popup = optionalPopup.get(); + Optional popUpIconDrawable = drawableCache.getDrawable(isDelayedPopup + ? popup.getPopUpLongPressIconResourceId() : popup.getPopUpIconResourceId()); + if (!popUpIconDrawable.isPresent()) { hidePopupView(); return; } - // Set images. - popupView.setImageDrawable(drawableCache.getDrawable(popup.getPopUpIconResourceId()).orNull()); + ImageView popupView = popUp.getContentView(); + Resources resources = popupView.getContext().getResources(); + float density = resources.getDisplayMetrics().density; + int popUpWindowPadding = (int) (BackgroundDrawableFactory.POPUP_WINDOW_PADDING * density); + int width = + Math.min(key.getWidth(), resources.getDimensionPixelSize(R.dimen.popup_width_limitation)) + + popUpWindowPadding * 2; + int height = popup.getHeight() + popUpWindowPadding * 2; + + popupView.setImageDrawable(popUpIconDrawable.get()); popupView.setBackgroundDrawable( backgroundDrawableFactory.getDrawable(DrawableType.POPUP_BACKGROUND_WINDOW)); - popupView.setPadding(padding, padding, padding, padding); - // Calculate the location to show the popup in window's coordinate system. + Preconditions.checkState(popup.getIconWidth() != 0 || popup.getIconHeight() != 0); + int horizontalPadding = (popup.getIconWidth() == 0) + ? popUpWindowPadding : (width - popup.getIconWidth()) / 2; + int verticalPadding = (popup.getIconHeight() == 0) + ? popUpWindowPadding : (height - popup.getIconHeight()) / 2; + popupView.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + + // Calculate the location to show the pop-up in window's coordinate system. int centerX = key.getX() + key.getWidth() / 2; int centerY = key.getY() + key.getHeight() / 2; - int width = popup.getWidth(); - int height = popup.getHeight(); - - ViewGroup.LayoutParams layoutParams = popupView.getLayoutParams(); - if (layoutParams != null) { - layoutParams.width = width; - layoutParams.height = height; - } - - if (MarginLayoutParams.class.isInstance(layoutParams)) { - int x = centerX + popup.getXOffset() - width / 2; - int y = centerY + popup.getYOffset() - height / 2; - - int[] location = new int[2]; - parent.getLocationInWindow(location); - x += location[0]; - y += location[1]; - - MarginLayoutParams marginLayoutParams = MarginLayoutParams.class.cast(layoutParams); - // Clip XY. - View root = View.class.cast(popupView.getParent()); - if (root != null) { - x = Math.max(Math.min(x, root.getWidth() - width), 0); - y = Math.max(Math.min(y, root.getHeight() - height), 0); - } - marginLayoutParams.setMargins(x, y, 0, 0); - } - popupView.setLayoutParams(layoutParams); + int left = centerX + popup.getXOffset() - width / 2; + int top = centerY + popup.getYOffset() - height / 2; + popUp.setBounds(left, top, left + width, top + height); popupView.setVisibility(View.VISIBLE); } /** * Hides the pop up preview. - * protected only for testing. */ - protected void dismiss() { + void dismiss() { hidePopupView(); } @SuppressWarnings("deprecation") private void hidePopupView() { + ImageView popupView = popUp.getContentView(); popupView.setVisibility(View.GONE); popupView.setImageDrawable(null); popupView.setBackgroundDrawable(null); diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/ProbableKeyEventGuesser.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/ProbableKeyEventGuesser.java index 9f646cfab..507388f38 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/ProbableKeyEventGuesser.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/ProbableKeyEventGuesser.java @@ -29,7 +29,6 @@ package org.mozc.android.inputmethod.japanese.keyboard; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard; import org.mozc.android.inputmethod.japanese.KeyboardSpecificationName; import org.mozc.android.inputmethod.japanese.MozcLog; import org.mozc.android.inputmethod.japanese.MozcUtil; @@ -64,8 +63,6 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; - /** * An object which guesses probable key events for typing correction feature. * @@ -90,7 +87,7 @@ public class ProbableKeyEventGuesser { */ @VisibleForTesting interface StatsFileAccessor { - InputStream openStream(JapaneseKeyboard japaneseKeyboard, Configuration configuration) + InputStream openStream(Keyboard keyboard, Configuration configuration) throws IOException; } @@ -104,8 +101,8 @@ static final class StatsFileAccessorImpl implements StatsFileAccessor { private final List assetFileNames; StatsFileAccessorImpl(AssetManager assetManager) { - this.assetManager = assetManager; - this.assetFileNames = getAssetFileNameList(assetManager); + this.assetManager = Preconditions.checkNotNull(assetManager); + this.assetFileNames = Preconditions.checkNotNull(getAssetFileNameList(assetManager)); } private static List getAssetFileNameList(AssetManager assetManager) { @@ -118,15 +115,17 @@ private static List getAssetFileNameList(AssetManager assetManager) { } @Override - public InputStream openStream(JapaneseKeyboard japaneseKeyboard, Configuration configuration) + public InputStream openStream(Keyboard keyboard, Configuration configuration) throws IOException { + Preconditions.checkNotNull(keyboard); + Preconditions.checkNotNull(configuration); + String baseName = - japaneseKeyboard.getSpecification().getKeyboardSpecificationName().baseName; + keyboard.getSpecification().getKeyboardSpecificationName().baseName; String orientation = KeyboardSpecificationName.getDeviceOrientationString(configuration); String fileName = String.format("%s_%s.touch_stats", baseName, orientation); - if (fileName.indexOf(File.separator) != -1) { - throw new IllegalArgumentException("fileName shouldn't include separator."); - } + Preconditions.checkArgument( + fileName.indexOf(File.separator) == -1, "fileName shouldn't include separator."); for (String file : assetFileNames) { if (file.equals(fileName)) { return assetManager.open(fileName); @@ -157,6 +156,8 @@ static final class LikelihoodCalculatorImpl implements LikelihoodCalculator { @Override public double getLikelihood(float firstX, float firstY, float deltaX, float deltaY, float[] probableEvent) { + Preconditions.checkNotNull(probableEvent); + float sdx = firstX - probableEvent[START_X_AVG]; float sdy = firstY - probableEvent[START_Y_AVG]; // Keys that are too far away from user's touch-down position @@ -198,32 +199,34 @@ interface UpdateStatsListener { void updateStats(String formattedKeyboardName, SparseArray stats); } - private final JapaneseKeyboard japaneseKeyboard; + private final Keyboard keyboard; private final Configuration configuration; private final StatsFileAccessor statsFileAccessor; private final UpdateStatsListener updateStatsListener; /** * @param statsFileAccessor an accessor for stats files. Must be non-null. - * @param japaneseKeyboard a {@link JapaneseKeyboard} to specify the file to be loaded. + * @param keyboard a {@link Keyboard} to specify the file to be loaded. * @param configuration a {@link Configuration} to specify the file to be loaded * @param formattedKeyboardNameToStats a {@link Map} to be updated. Must be non-null. * @param updateStatsExecutor an Executor on which the result is propagated */ private StatisticsLoader( StatsFileAccessor statsFileAccessor, - JapaneseKeyboard japaneseKeyboard, + Keyboard keyboard, Configuration configuration, final Map> formattedKeyboardNameToStats, final Executor updateStatsExecutor) { this( statsFileAccessor, - japaneseKeyboard, + keyboard, configuration, new UpdateStatsListener() { @Override public void updateStats(final String formattedKeyboardName, final SparseArray stats) { + Preconditions.checkNotNull(formattedKeyboardName); + Preconditions.checkNotNull(stats); updateStatsExecutor.execute(new Runnable() { @Override public void run() { @@ -234,14 +237,13 @@ public void run() { }); } - // For testing purpose. @VisibleForTesting StatisticsLoader(StatsFileAccessor statsFileAccessor, - JapaneseKeyboard japaneseKeyboard, + Keyboard keyboard, Configuration configuration, UpdateStatsListener updateStatsListener) { this.statsFileAccessor = Preconditions.checkNotNull(statsFileAccessor); - this.japaneseKeyboard = Preconditions.checkNotNull(japaneseKeyboard); + this.keyboard = Preconditions.checkNotNull(keyboard); this.configuration = Preconditions.checkNotNull(configuration); this.updateStatsListener = Preconditions.checkNotNull(updateStatsListener); } @@ -275,7 +277,7 @@ public void run() { SparseArray result = new SparseArray(MAX_KEY_NUMBER_IN_KEYBOARD); InputStream inputStream = null; try { - inputStream = statsFileAccessor.openStream(japaneseKeyboard, configuration); + inputStream = statsFileAccessor.openStream(keyboard, configuration); readStream(new DataInputStream(inputStream), result); } catch (IOException e) { MozcLog.d("Stream access fails.", e); @@ -290,7 +292,7 @@ public void run() { } } // Successfully loaded the file so call back updateStatsListener. - String formattedKeyboardName = japaneseKeyboard.getSpecification() + String formattedKeyboardName = keyboard.getSpecification() .getKeyboardSpecificationName().formattedKeyboardName(configuration); updateStatsListener.updateStats(formattedKeyboardName, result); } @@ -351,14 +353,14 @@ public void run() { // Invoked from the thread which dataLoadExecutor uses. private final Executor dataPropagationExecutor; - // Current JapaneseKeyboard. - private Optional japaneseKeyboard = Optional.absent(); + // Current Keyboard. + private Optional keyboard = Optional.absent(); // Current Configuration. private Optional configuration = Optional.absent(); // Formatted keyboard name. - // This can be calculated from japaneseKeyboard and configuration so is derivative variable. + // This can be calculated from keyboard and configuration so is derivative variable. // Just for cache. private Optional formattedKeyboardName = Optional.absent(); @@ -410,17 +412,16 @@ public void execute(Runnable command) { } /** - * Sets a {@link JapaneseKeyboard}. + * Sets a {@link Keyboard}. * * This invocation might load a stats data file (typically) asynchronously. * Before the loading has finished * {@link #getProbableKeyEvents(List)} cannot return any probable key events. * @see #getProbableKeyEvents(List) - * @param japaneseKeyboard a {@link JapaneseKeyboard} to be set. - * If null {@link #getProbableKeyEvents(List)} will return null. + * @param keyboard a {@link Keyboard} to be set. */ - public void setJapaneseKeyboard(@Nullable JapaneseKeyboard japaneseKeyboard) { - this.japaneseKeyboard = Optional.fromNullable(japaneseKeyboard); + public void setKeyboard(Keyboard keyboard) { + this.keyboard = Optional.of(keyboard); updateFormattedKeyboardName(); maybeUpdateEventStatistics(); } @@ -428,11 +429,11 @@ public void setJapaneseKeyboard(@Nullable JapaneseKeyboard japaneseKeyboard) { /** * Sets a {@link Configuration}. * - * Behaves almost the same as {@link #setJapaneseKeyboard(JapaneseKeyboard)}. + * Behaves almost the same as {@link #setKeyboard(Keyboard)}. * @param configuration a {@link Configuration} to be set. */ - public void setConfiguration(@Nullable Configuration configuration) { - this.configuration = Optional.fromNullable(configuration); + public void setConfiguration(Optional configuration) { + this.configuration = Preconditions.checkNotNull(configuration); updateFormattedKeyboardName(); maybeUpdateEventStatistics(); } @@ -441,11 +442,11 @@ public void setConfiguration(@Nullable Configuration configuration) { * Updates (cached) {@link #formattedKeyboardName}. */ private void updateFormattedKeyboardName() { - if (!japaneseKeyboard.isPresent() || !configuration.isPresent()) { + if (!keyboard.isPresent() || !configuration.isPresent()) { formattedKeyboardName = Optional.absent(); return; } - formattedKeyboardName = Optional.of(japaneseKeyboard.get() + formattedKeyboardName = Optional.of(keyboard.get() .getSpecification() .getKeyboardSpecificationName() .formattedKeyboardName( @@ -453,7 +454,7 @@ private void updateFormattedKeyboardName() { } /** - * If stats for current JapaneseKeyboard and Configuration has not been loaded, + * If stats for current {@code Keyboard} and {@code Configuration} has not been loaded, * load and update it asynchronously. * * Loading queue contains only the latest task. @@ -461,7 +462,7 @@ private void updateFormattedKeyboardName() { */ private void maybeUpdateEventStatistics() { if (!formattedKeyboardName.isPresent() - || !japaneseKeyboard.isPresent() + || !keyboard.isPresent() || !configuration.isPresent()) { return; } @@ -471,7 +472,7 @@ private void maybeUpdateEventStatistics() { } dataLoadExecutor.execute( new StatisticsLoader( - statsFileAccessor, japaneseKeyboard.get(), configuration.get(), + statsFileAccessor, keyboard.get(), configuration.get(), formattedKeyboardNameToStats, dataPropagationExecutor)); } @@ -480,25 +481,25 @@ private void maybeUpdateEventStatistics() { /** * Calculates probable key events for given {@code touchEventList}. * - * A List is returned (null will not be returned). * If corresponding stats data has not been loaded yet (file access is done asynchronously), * empty list is returned. * * TODO(matsuzakit): Change the caller side which expects null-return-value, * before submitting this CL. * - * @param touchEventList a List of TouchEvents. If null, empty List is returned. + * @param touchEventList a List of TouchEvents. */ - public List getProbableKeyEvents( - @Nullable List touchEventList) { + public List getProbableKeyEvents(List touchEventList) { + Preconditions.checkNotNull(touchEventList); + // This method's responsibility is to pre-check the condition. // Calculation itself is done in getProbableKeyEventsInternal method. - if (!japaneseKeyboard.isPresent() || !configuration.isPresent()) { + if (!keyboard.isPresent() || !configuration.isPresent()) { // Keyboard has not been set up. return Collections.emptyList(); } - if (touchEventList == null || touchEventList.size() != 1) { - // If null || size == 0 , we can do nothing. + if (touchEventList.size() != 1) { + // If size == 0 , we can do nothing. // If size >= 2, this is special situation (saturating touch event) so do nothing. return Collections.emptyList(); } @@ -507,7 +508,7 @@ public List getProbableKeyEvents( if (eventStatistics == null) { // No corresponding stats is available. Returning null. // The stats we need might be pushed out from the LRU cache because of bulk-updates of - // JapaneseKeyboard or Configuration. + // Keyboard or Configuration. // For such case we invoke maybeUpdateEventStatistics method to load the stats. // This is very rare (and usually not happen) but just in case. maybeUpdateEventStatistics(); @@ -566,13 +567,14 @@ public List getProbableKeyEvents( private SparseArray getLikelihoodArray(SparseArray eventStatistics, float firstX, float firstY, float deltaX, float deltaY) { - Preconditions.checkState(japaneseKeyboard.isPresent()); + Preconditions.checkNotNull(eventStatistics); + Preconditions.checkState(keyboard.isPresent()); SparseArray result = new SparseArray(eventStatistics.size()); - JapaneseKeyboard japaneseKeyboard = this.japaneseKeyboard.get(); + Keyboard keyboard = this.keyboard.get(); for (int i = 0; i < eventStatistics.size(); ++i) { int sourceId = eventStatistics.keyAt(i); - int keyCode = japaneseKeyboard.getKeyCode(sourceId); + int keyCode = keyboard.getKeyCode(sourceId); // Special key or non-existent-key. // Don't produce probable key events. if (keyCode <= 0) { diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Row.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Row.java index 611d80798..08a1bd299 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Row.java +++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Row.java @@ -29,6 +29,8 @@ package org.mozc.android.inputmethod.japanese.keyboard; +import com.google.common.base.Preconditions; + import java.util.Collections; import java.util.List; @@ -50,7 +52,7 @@ public class Row { private final int verticalGap; public Row(List keyList, int height, int verticalGap) { - this.keyList = Collections.unmodifiableList(keyList); + this.keyList = Collections.unmodifiableList(Preconditions.checkNotNull(keyList)); this.height = height; this.verticalGap = verticalGap; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/model/JapaneseSoftwareKeyboardModel.java b/src/android/src/com/google/android/inputmethod/japanese/model/JapaneseSoftwareKeyboardModel.java index df2a0aed7..57cc82ec0 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/model/JapaneseSoftwareKeyboardModel.java +++ b/src/android/src/com/google/android/inputmethod/japanese/model/JapaneseSoftwareKeyboardModel.java @@ -29,8 +29,9 @@ package org.mozc.android.inputmethod.japanese.model; -import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.MozcLog; +import org.mozc.android.inputmethod.japanese.MozcUtil; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.InputStyle; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; import com.google.common.base.Optional; @@ -50,22 +51,19 @@ *

  • {@code qwertyLayoutForAlphabet} *
* - * We have two {@code KeyboardLayout}, which are {@code TWELVE_KEY} and {@code QWERTY}, - * and one experimental layout, {@code GODAN}. - * * For {@code TWLEVE_KEY} layout, we have three {@code InputStyle}, which are {@code TOGGLE}, * {@code FLICK} and {@code TOGGLE_FLICK}. * Also, users can use {@code QWERTY} style layout for alphabet mode by setting * {@code qwertyLayoutForAlphabet} {@code true}. * - * {@code TWLEVE_KEY} layout has mainly three {@code KeyboardMode}, which are {@code KANA}, - * {@code ALPHABET} and {@code KANA_NUMBER}. If a user uses {@code QWERTY} style layout for - * alphabet mode, s/he can use {@code ALPHABET_NUMBER} mode, which is {@code QWERTY} style number - * key layout, as well in addition to the three {@code KeyboardMode}. + * {@code TWLEVE_KEY} layout has two {@code KeyboardMode}, which are {@code KANA}, {@code ALPHABET}. + * + * On {@code SymbolInputView}, we have a special {@code KeyboardMode}, which is + * {@code SYMBOL_NUMBER}. It is NOT used on normal view. * - * For {@code QWERTY} layout, we have four {@code KeyboardMode}, which are {@code KANA}, - * {@code ALPHABET}, {@code KANA_NUMBER} and {@code ALPHABET_NUMBER}. The parameters, - * {@code InputStyle} and {@code qwertyLayoutForAlphabet}, are simply ignored. + * For {@code QWERTY} layout, we have two {@code KeyboardMode}, which are {@code KANA}, + * {@code ALPHABET}. The parameters, {@code InputStyle} and {@code qwertyLayoutForAlphabet}, are + * simply ignored. * * This class manages the "default mode" of software keyboard depending on {@code inputType}. * It is expected that the {@code inputType} is given by system via @@ -79,7 +77,7 @@ public class JapaneseSoftwareKeyboardModel { * Keyboard mode that indicates supported character types. */ public enum KeyboardMode { - KANA, ALPHABET, KANA_NUMBER, ALPHABET_NUMBER, + KANA, ALPHABET, ALPHABET_NUMBER, NUMBER, SYMBOL_NUMBER, } private KeyboardLayout keyboardLayout = KeyboardLayout.TWELVE_KEYS; @@ -160,24 +158,24 @@ public void setInputType(int inputType) { } } - private static Optional getPreferredKeyboardMode( + public static Optional getPreferredKeyboardMode( int inputType, KeyboardLayout layout) { - Preconditions.checkNotNull(layout); - switch (inputType & InputType.TYPE_MASK_CLASS) { - case InputType.TYPE_CLASS_DATETIME: - case InputType.TYPE_CLASS_PHONE: - case InputType.TYPE_CLASS_NUMBER: - return layout == KeyboardLayout.TWELVE_KEYS - ? Optional.of(KeyboardMode.KANA_NUMBER) - : Optional.of(KeyboardMode.ALPHABET_NUMBER); - case InputType.TYPE_CLASS_TEXT: - switch (inputType & InputType.TYPE_MASK_VARIATION) { - case InputType.TYPE_TEXT_VARIATION_PASSWORD: - case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: - case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: - case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: - return Optional.of(KeyboardMode.ALPHABET); - } + if (MozcUtil.isNumberKeyboardPreferred(inputType)) { + switch (Preconditions.checkNotNull(layout)) { + case GODAN: + case QWERTY: + case TWELVE_KEYS: + return Optional.of(KeyboardMode.NUMBER); + } + } + if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { + switch (inputType & InputType.TYPE_MASK_VARIATION) { + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + return Optional.of(KeyboardMode.ALPHABET); + } } // KeyboardMode recommended strongly is not found here, so just return null. return Optional.absent(); @@ -218,9 +216,7 @@ private static KeyboardSpecification getKeyboardSpecificationInternal( } private static KeyboardSpecification getTwelveKeysKeyboardSpecification( - KeyboardMode keyboardMode, - InputStyle inputStyle, - boolean qwertyLayoutForAlphabet) { + KeyboardMode keyboardMode, InputStyle inputStyle, boolean qwertyLayoutForAlphabet) { switch (keyboardMode) { case KANA: { switch (inputStyle) { @@ -252,14 +248,10 @@ private static KeyboardSpecification getTwelveKeysKeyboardSpecification( } break; } - case KANA_NUMBER: { - switch (inputStyle) { - case TOGGLE: return KeyboardSpecification.TWELVE_KEY_TOGGLE_NUMBER; - case FLICK: return KeyboardSpecification.TWELVE_KEY_FLICK_NUMBER; - case TOGGLE_FLICK: return KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_NUMBER; - } - break; - } + case NUMBER: + return KeyboardSpecification.NUMBER; + case SYMBOL_NUMBER: + return KeyboardSpecification.SYMBOL_NUMBER; } throw new IllegalArgumentException( "Unknown keyboard state: " @@ -269,9 +261,10 @@ private static KeyboardSpecification getTwelveKeysKeyboardSpecification( private static KeyboardSpecification getQwertyKeyboardSpecification(KeyboardMode keyboardMode) { switch (keyboardMode) { case KANA: return KeyboardSpecification.QWERTY_KANA; - case KANA_NUMBER: return KeyboardSpecification.QWERTY_KANA_NUMBER; case ALPHABET: return KeyboardSpecification.QWERTY_ALPHABET; case ALPHABET_NUMBER: return KeyboardSpecification.QWERTY_ALPHABET_NUMBER; + case NUMBER: return KeyboardSpecification.NUMBER; + case SYMBOL_NUMBER: return KeyboardSpecification.SYMBOL_NUMBER; } throw new IllegalArgumentException("Unknown keyboard mode: " + keyboardMode); } @@ -281,10 +274,10 @@ private static KeyboardSpecification getGodanKeyboardSpecification(KeyboardMode case KANA: return KeyboardSpecification.GODAN_KANA; case ALPHABET: return KeyboardSpecification.QWERTY_ALPHABET; case ALPHABET_NUMBER: return KeyboardSpecification.QWERTY_ALPHABET_NUMBER; - default: - // KANA_NUMBER must be never used. - throw new IllegalArgumentException("Unknown keyboard mode: " + keyboardMode); + case NUMBER: return KeyboardSpecification.NUMBER; + case SYMBOL_NUMBER: return KeyboardSpecification.SYMBOL_NUMBER; } + throw new IllegalArgumentException("Unknown keyboard mode: " + keyboardMode); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/model/SymbolCandidateStorage.java b/src/android/src/com/google/android/inputmethod/japanese/model/SymbolCandidateStorage.java index 4c1ce91ea..e98504b5d 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/model/SymbolCandidateStorage.java +++ b/src/android/src/com/google/android/inputmethod/japanese/model/SymbolCandidateStorage.java @@ -226,6 +226,10 @@ private static void createEmojiDescriptionMapInternal( /** @return the {@link CandidateList} instance for the given {@code minorCategory}. */ public CandidateList getCandidateList(SymbolMinorCategory minorCategory) { switch (minorCategory) { + // NUMBER major category candidates. + case NUMBER: + return CandidateList.getDefaultInstance(); + // SYMBOL major category candidates. case SYMBOL_HISTORY: return toCandidateList(symbolHistoryStorage.getAllHistory(SymbolMajorCategory.SYMBOL)); diff --git a/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMajorCategory.java b/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMajorCategory.java index 7f7ef3680..2a5dc18e2 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMajorCategory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMajorCategory.java @@ -30,18 +30,29 @@ package org.mozc.android.inputmethod.japanese.model; import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer; +import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.DescriptionLayoutPolicy; import com.google.common.base.Preconditions; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** * Symbol's major category to which the minor categories belong. */ public enum SymbolMajorCategory { + NUMBER( + R.id.category_selector_major_number, + R.raw.symbol__major__number, + R.dimen.symbol_major_number_height, + Collections.singletonList(SymbolMinorCategory.NUMBER), + R.dimen.symbol_view_symbol_min_column_width, + DescriptionLayoutPolicy.GONE), // DescriptionLayoutPolicy is not effective for NUMBER. SYMBOL( R.id.category_selector_major_symbol, R.raw.symbol__major__symbol, + R.dimen.symbol_major_symbol_height, Arrays.asList( SymbolMinorCategory.SYMBOL_HISTORY, SymbolMinorCategory.SYMBOL_GENERAL, @@ -50,10 +61,12 @@ public enum SymbolMajorCategory { SymbolMinorCategory.SYMBOL_ARROW, SymbolMinorCategory.SYMBOL_MATH ), - R.dimen.symbol_view_symbol_min_column_width), + R.dimen.symbol_view_symbol_min_column_width, + DescriptionLayoutPolicy.OVERLAY), EMOTICON( R.id.category_selector_major_emoticon, R.raw.symbol__major__emoticon, + R.dimen.symbol_major_emoticon_height, Arrays.asList( SymbolMinorCategory.EMOTICON_HISTORY, SymbolMinorCategory.EMOTICON_SMILE, @@ -62,10 +75,12 @@ public enum SymbolMajorCategory { SymbolMinorCategory.EMOTICON_SADNESS, SymbolMinorCategory.EMOTICON_DISPLEASURE ), - R.dimen.symbol_view_emoticon_min_column_width), + R.dimen.symbol_view_emoticon_min_column_width, + DescriptionLayoutPolicy.OVERLAY), EMOJI( R.id.category_selector_major_emoji, R.raw.symbol__major__emoji, + R.dimen.symbol_major_emoji_height, Arrays.asList( SymbolMinorCategory.EMOJI_HISTORY, SymbolMinorCategory.EMOJI_FACE, @@ -74,14 +89,17 @@ public enum SymbolMajorCategory { SymbolMinorCategory.EMOJI_CITY, SymbolMinorCategory.EMOJI_NATURE ), - R.dimen.symbol_view_emoji_min_column_width) + R.dimen.symbol_view_emoji_min_column_width, + DescriptionLayoutPolicy.GONE) // Description is not show in the light of UX ; // All fields are invariant so access directly. public final int buttonResourceId; public final int buttonImageResourceId; + public final int maxImageHeightResourceId; public final List minorCategories; public final int minColumnWidthResourceId; + public final DescriptionLayoutPolicy layoutPolicy; /** * @param buttonResourceId the resource id (R.id.xxxx) of corresponding selector button. @@ -95,14 +113,18 @@ public enum SymbolMajorCategory { private SymbolMajorCategory( int buttonResourceId, int buttonImageResourceId, + int maxImageHeightResourceId, List minorCategories, - int minColumnWidthResourceId) { + int minColumnWidthResourceId, + CandidateLayoutRenderer.DescriptionLayoutPolicy layoutPolicy) { // We just store resource id instead of real bitmap/integer // because we cannot obtain them here (Context instance is needed). this.buttonResourceId = buttonResourceId; this.buttonImageResourceId = buttonImageResourceId; + this.maxImageHeightResourceId = maxImageHeightResourceId; this.minorCategories = Preconditions.checkNotNull(minorCategories); this.minColumnWidthResourceId = minColumnWidthResourceId; + this.layoutPolicy = layoutPolicy; } /** @@ -116,7 +138,7 @@ public SymbolMinorCategory getDefaultMinorCategory() { } public SymbolMinorCategory getMinorCategoryByRelativeIndex(SymbolMinorCategory minorCategory, - int relativeIndex) { + int relativeIndex) { int index = minorCategories.indexOf(Preconditions.checkNotNull(minorCategory)); int newIndex = (index + relativeIndex + minorCategories.size()) % minorCategories.size(); diff --git a/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMinorCategory.java b/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMinorCategory.java index 3eff2bb4f..9335aa703 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMinorCategory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/model/SymbolMinorCategory.java @@ -35,32 +35,59 @@ * Symbol's minor category to which the candidates belong. */ public enum SymbolMinorCategory { - SYMBOL_HISTORY(R.string.symbol_minor_symbol_history_title), - SYMBOL_GENERAL(R.string.symbol_minor_symbol_general_title), - SYMBOL_HALF(R.string.symbol_minor_symbol_half_title), - SYMBOL_PARENTHESIS(R.string.symbol_minor_symbol_parenthesis_title), - SYMBOL_ARROW(R.string.symbol_minor_symbol_arrow_title), - SYMBOL_MATH(R.string.symbol_minor_symbol_math_title), - EMOTICON_HISTORY(R.string.symbol_minor_emoticon_history_title), - EMOTICON_SMILE(R.string.symbol_minor_emoticon_smile_title), - EMOTICON_SWEAT(R.string.symbol_minor_emoticon_sweat_title), - EMOTICON_SURPRISE(R.string.symbol_minor_emoticon_surprise_title), - EMOTICON_SADNESS(R.string.symbol_minor_emoticon_sadness_title), - EMOTICON_DISPLEASURE(R.string.symbol_minor_emoticon_displeasure_title), - EMOJI_HISTORY(R.string.symbol_minor_emoji_history_title), - EMOJI_FACE(R.string.symbol_minor_emoji_face_title), - EMOJI_FOOD(R.string.symbol_minor_emoji_food_title), - EMOJI_ACTIVITY(R.string.symbol_minor_emoji_activity_title), - EMOJI_CITY(R.string.symbol_minor_emoji_city_title), - EMOJI_NATURE(R.string.symbol_minor_emoji_nature_title) + NUMBER(SymbolMinorCategory.INVALID_RESOURCE_ID, SymbolMinorCategory.INVALID_RESOURCE_ID, + SymbolMinorCategory.INVALID_RESOURCE_ID), + SYMBOL_HISTORY(R.raw.symbol__minor__history, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_history), + SYMBOL_GENERAL(R.raw.symbol__minor__general, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_symbol_general), + SYMBOL_HALF(R.raw.symbol__minor__fullhalf, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_symbol_half), + SYMBOL_PARENTHESIS(R.raw.symbol__minor__parenthesis, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_symbol_parenthesis), + SYMBOL_ARROW(R.raw.symbol__minor__arrow, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_symbol_arrow), + SYMBOL_MATH(R.raw.symbol__minor__math, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_symbol_math), + EMOTICON_HISTORY(R.raw.symbol__minor__history, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_history), + EMOTICON_SMILE(R.raw.symbol__minor__smile, R.dimen.symbol_minor_emoticon_height, + R.string.cd_symbol_window_minor_emoticon_smile), + EMOTICON_SWEAT(R.raw.symbol__minor__sweat, R.dimen.symbol_minor_emoticon_height, + R.string.cd_symbol_window_minor_emoticon_sweat), + EMOTICON_SURPRISE(R.raw.symbol__minor__surprise, R.dimen.symbol_minor_emoticon_height, + R.string.cd_symbol_window_minor_emoticon_surprise), + EMOTICON_SADNESS(R.raw.symbol__minor__sadness, R.dimen.symbol_minor_emoticon_height, + R.string.cd_symbol_window_minor_emoticon_sadness), + EMOTICON_DISPLEASURE(R.raw.symbol__minor__displeasure, R.dimen.symbol_minor_emoticon_height, + R.string.cd_symbol_window_minor_emoticon_displeasure), + EMOJI_HISTORY(R.raw.symbol__minor__history, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_history), + EMOJI_FACE(R.raw.symbol__minor__face, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_emoji_face), + EMOJI_FOOD(R.raw.symbol__minor__food, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_emoji_food), + EMOJI_ACTIVITY(R.raw.symbol__minor__activity, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_emoji_activity), + EMOJI_CITY(R.raw.symbol__minor__city, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_emoji_city), + EMOJI_NATURE(R.raw.symbol__minor__nature, R.dimen.symbol_minor_default_height, + R.string.cd_symbol_window_minor_emoji_nature) ; - public final int textResourceId; + public static final int INVALID_RESOURCE_ID = 0; + public final int drawableResourceId; + public final int maxImageHeightResourceId; + public final int contentDescriptionResourceId; /** - * @param textResourceId the resource id of the corresponding text title. + * @param drawableResourceId the resource id of the corresponding drawable. + * @param contentDescriptionResourceId the resource id of the corresponding content description. */ - SymbolMinorCategory(int textResourceId) { - this.textResourceId = textResourceId; + SymbolMinorCategory(int drawableResourceId, int maxImageHeightResourceId, + int contentDescriptionResourceId) { + this.drawableResourceId = drawableResourceId; + this.maxImageHeightResourceId = maxImageHeightResourceId; + this.contentDescriptionResourceId = contentDescriptionResourceId; } } \ No newline at end of file diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/ClientSidePreference.java b/src/android/src/com/google/android/inputmethod/japanese/preference/ClientSidePreference.java index 7f47fa739..250326f8e 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/ClientSidePreference.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/ClientSidePreference.java @@ -31,12 +31,14 @@ import org.mozc.android.inputmethod.japanese.ViewManagerInterface.LayoutAdjustment; import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; +import org.mozc.android.inputmethod.japanese.resources.R; import org.mozc.android.inputmethod.japanese.view.SkinType; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.content.res.Resources; /** * This class expresses the client-side preferences which corresponds to current @@ -87,7 +89,6 @@ public enum InputStyle { public enum HardwareKeyMap { DEFAULT, JAPANESE109A, - TWELVEKEY, } private final boolean isHapticFeedbackEnabled; @@ -103,6 +104,7 @@ public enum HardwareKeyMap { private final EmojiProviderType emojiProviderType; private final HardwareKeyMap hardwareKeyMap; private final SkinType skinType; + private final boolean isMicrophoneButtonEnabled; private final LayoutAdjustment layoutAdjustment; /** Percentage of keyboard height */ @@ -110,14 +112,15 @@ public enum HardwareKeyMap { /** * If you want to use this method, - * consider using {@link #ClientSidePreference(SharedPreferences, int)} instead. + * consider using {@link #ClientSidePreference(SharedPreferences, Resources, int)} instead. */ @VisibleForTesting public ClientSidePreference( boolean isHapticFeedbackEnabled, long hapticFeedbackDuration, boolean isSoundFeedbackEnabled, int soundFeedbackVolume, boolean isPopupFeedbackEnabled, KeyboardLayout keyboardLayout, InputStyle inputStyle, boolean qwertyLayoutForAlphabet, boolean fullscreenMode, int flickSensitivity, EmojiProviderType emojiProviderType, HardwareKeyMap hardwareKeyMap, - SkinType skinType, LayoutAdjustment layoutAdjustment, int keyboardHeightRatio) { + SkinType skinType, boolean isMicrophoneButtonEnabled, LayoutAdjustment layoutAdjustment, + int keyboardHeightRatio) { this.isHapticFeedbackEnabled = isHapticFeedbackEnabled; this.hapticFeedbackDuration = hapticFeedbackDuration; this.isSoundFeedbackEnabled = isSoundFeedbackEnabled; @@ -131,11 +134,13 @@ public enum HardwareKeyMap { this.emojiProviderType = Preconditions.checkNotNull(emojiProviderType); this.hardwareKeyMap = Preconditions.checkNotNull(hardwareKeyMap); this.skinType = Preconditions.checkNotNull(skinType); + this.isMicrophoneButtonEnabled = isMicrophoneButtonEnabled; this.layoutAdjustment = Preconditions.checkNotNull(layoutAdjustment); this.keyboardHeightRatio = keyboardHeightRatio; } - public ClientSidePreference(SharedPreferences sharedPreferences, int deviceOrientation) { + public ClientSidePreference( + SharedPreferences sharedPreferences, Resources resources, int deviceOrientation) { Preconditions.checkNotNull(sharedPreferences); isHapticFeedbackEnabled = @@ -148,6 +153,8 @@ public ClientSidePreference(SharedPreferences sharedPreferences, int deviceOrien sharedPreferences.getInt(PreferenceUtil.PREF_SOUND_FEEDBACK_VOLUME_KEY, 50); isPopupFeedbackEnabled = sharedPreferences.getBoolean(PreferenceUtil.PREF_POPUP_FEEDBACK_KEY, true); + isMicrophoneButtonEnabled = + sharedPreferences.getBoolean(PreferenceUtil.PREF_VOICE_INPUT_KEY, true); String keyboardLayoutKey; String inputStyleKey; @@ -197,9 +204,11 @@ public ClientSidePreference(SharedPreferences sharedPreferences, int deviceOrien EmojiProviderType.NONE); hardwareKeyMap = PreferenceUtil.getEnum( - sharedPreferences, PreferenceUtil.PREF_HARDWARE_KEYMAP, HardwareKeyMap.class, null); + sharedPreferences, PreferenceUtil.PREF_HARDWARE_KEYMAP, HardwareKeyMap.class, + HardwareKeyMap.DEFAULT); skinType = PreferenceUtil.getEnum( - sharedPreferences, PreferenceUtil.PREF_SKIN_TYPE, SkinType.class, SkinType.BLUE_LIGHTGRAY); + sharedPreferences, resources.getString(R.string.pref_skin_type_key), + SkinType.class, SkinType.valueOf(resources.getString(R.string.pref_skin_type_default))); layoutAdjustment = PreferenceUtil.getEnum( sharedPreferences, layoutAdjustmentKey, LayoutAdjustment.class, LayoutAdjustment.FILL); keyboardHeightRatio = sharedPreferences.getInt(keyboardHeightRatioKey, 100); @@ -257,6 +266,10 @@ public SkinType getSkinType() { return skinType; } + public boolean isMicrophoneButtonEnabled() { + return isMicrophoneButtonEnabled; + } + public LayoutAdjustment getLayoutAdjustment() { return layoutAdjustment; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardLayoutPreference.java b/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardLayoutPreference.java index 3b6db3bdf..f17a3d1e5 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardLayoutPreference.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardLayoutPreference.java @@ -30,9 +30,12 @@ package org.mozc.android.inputmethod.japanese.preference; import org.mozc.android.inputmethod.japanese.MozcLog; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.view.Skin; import org.mozc.android.inputmethod.japanese.view.SkinType; +import com.google.common.base.Preconditions; import android.content.Context; import android.content.SharedPreferences; @@ -67,13 +70,13 @@ public class KeyboardLayoutPreference extends Preference { static class Item { final KeyboardLayout keyboardLayout; - final int keyboardResourceId; + final KeyboardSpecification specification; final int titleResId; final int descriptionResId; - Item(KeyboardLayout keyboardLayout, int keyboardResourceId, + Item(KeyboardLayout keyboardLayout, KeyboardSpecification specification, int titleResId, int descriptionResId) { this.keyboardLayout = keyboardLayout; - this.keyboardResourceId = keyboardResourceId; + this.specification = specification; this.titleResId = titleResId; this.descriptionResId = descriptionResId; } @@ -87,7 +90,7 @@ class ImageAdapter extends BaseAdapter { for (Item item : itemList) { drawableMap.put( item.keyboardLayout, - new KeyboardPreviewDrawable(resources, item.keyboardLayout, item.keyboardResourceId)); + new KeyboardPreviewDrawable(resources, item.keyboardLayout, item.specification)); } } @@ -136,9 +139,10 @@ public View getView(int position, View convertView, ViewGroup parentView) { return convertView; } - void setSkinType(SkinType skinType) { + void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); for (KeyboardPreviewDrawable drawable : drawableMap.values()) { - drawable.setSkinType(skinType); + drawable.setSkin(skin); } } } @@ -175,17 +179,17 @@ public void onNothingSelected(AdapterView parent) { static final List itemList = Collections.unmodifiableList(Arrays.asList( new Item( KeyboardLayout.TWELVE_KEYS, - R.xml.kbd_12keys_flick_kana, + KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA, R.string.pref_keyboard_layout_title_12keys, R.string.pref_keyboard_layout_description_12keys), new Item( KeyboardLayout.QWERTY, - R.xml.kbd_qwerty_kana, + KeyboardSpecification.QWERTY_KANA, R.string.pref_keyboard_layout_title_qwerty, R.string.pref_keyboard_layout_description_qwerty), new Item( KeyboardLayout.GODAN, - R.xml.kbd_godan_kana, + KeyboardSpecification.GODAN_KANA, R.string.pref_keyboard_layout_title_godan, R.string.pref_keyboard_layout_description_godan))); @@ -195,8 +199,8 @@ public void onNothingSelected(AdapterView parent) { new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(PreferenceUtil.PREF_SKIN_TYPE)) { - updateSkinType(); + if (key.equals(getContext().getResources().getString(R.string.pref_skin_type_key))) { + updateSkin(); } } }; @@ -288,7 +292,7 @@ protected void onBindView(View view) { gallery.setOnItemClickListener(galleryEventListener); gallery.setSelection(getActiveIndex()); - updateSkinType(); + updateSkin(); } private static void updateAllItemBackground(AdapterView gallery, int activeIndex) { @@ -311,7 +315,7 @@ private static void updateBackground(View view, int position, int activePosition @Override protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); - updateSkinType(); + updateSkin(); getSharedPreferences().registerOnSharedPreferenceChangeListener( sharedPreferenceChangeListener); } @@ -323,10 +327,12 @@ protected void onPrepareForRemoval() { super.onPrepareForRemoval(); } - void updateSkinType() { + void updateSkin() { + Resources resources = getContext().getResources(); SkinType skinType = PreferenceUtil.getEnum( getSharedPreferences(), - PreferenceUtil.PREF_SKIN_TYPE, SkinType.class, SkinType.ORANGE_LIGHTGRAY); - imageAdapter.setSkinType(skinType); + resources.getString(R.string.pref_skin_type_key), + SkinType.class, SkinType.valueOf(resources.getString(R.string.pref_skin_type_default))); + imageAdapter.setSkin(skinType.getSkin(resources)); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardPreviewDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardPreviewDrawable.java index 94c5f87b6..6c113c581 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardPreviewDrawable.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/KeyboardPreviewDrawable.java @@ -34,14 +34,14 @@ import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory; import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState; import org.mozc.android.inputmethod.japanese.keyboard.Keyboard; +import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; import org.mozc.android.inputmethod.japanese.keyboard.KeyboardParser; import org.mozc.android.inputmethod.japanese.keyboard.KeyboardViewBackgroundSurface; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; import org.mozc.android.inputmethod.japanese.resources.R; import org.mozc.android.inputmethod.japanese.view.DrawableCache; -import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; -import org.mozc.android.inputmethod.japanese.view.SkinType; -import com.google.common.base.Optional; +import org.mozc.android.inputmethod.japanese.view.Skin; +import com.google.common.base.Preconditions; import android.content.res.Resources; import android.graphics.Bitmap; @@ -51,8 +51,6 @@ import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; -import android.graphics.Shader.TileMode; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import org.xmlpull.v1.XmlPullParserException; @@ -63,6 +61,8 @@ import java.util.Map; import java.util.WeakHashMap; +import javax.annotation.Nullable; + /** * A Drawable to render keyboard preview. * @@ -118,7 +118,7 @@ static class BitmapCache { private final Map map = new EnumMap(KeyboardLayout.class); - private SkinType skinType = null; + private Skin skin = Skin.getFallbackInstance(); private final WeakHashMap referenceMap = new WeakHashMap(); @@ -129,12 +129,13 @@ static BitmapCache getInstance() { return INSTANCE; } - Bitmap get(KeyboardLayout keyboardLayout, int width, int height, SkinType skinType) { - if (keyboardLayout == null || width <= 0 || height <= 0 || skinType == null) { + @Nullable Bitmap get(KeyboardLayout keyboardLayout, int width, int height, Skin skin) { + Preconditions.checkNotNull(skin); + if (keyboardLayout == null || width <= 0 || height <= 0) { return null; } - if (skinType != this.skinType) { + if (!skin.equals(this.skin)) { return null; } @@ -149,14 +150,15 @@ Bitmap get(KeyboardLayout keyboardLayout, int width, int height, SkinType skinTy return result; } - void put(KeyboardLayout keyboardLayout, SkinType skinType, Bitmap bitmap) { - if (keyboardLayout == null || skinType == null || bitmap == null) { + void put(@Nullable KeyboardLayout keyboardLayout, Skin skin, @Nullable Bitmap bitmap) { + Preconditions.checkNotNull(skin); + if (keyboardLayout == null || bitmap == null) { return; } - if (skinType != this.skinType) { + if (!skin.equals(this.skin)) { clear(); - this.skinType = skinType; + this.skin = skin; } Bitmap oldBitmap = map.put(keyboardLayout, bitmap); @@ -187,23 +189,23 @@ private void clear() { } map.clear(); - skinType = null; + skin = Skin.getFallbackInstance(); } } private final Resources resources; private final KeyboardLayout keyboardLayout; - private final int keyboardResourceId; + private final KeyboardSpecification specification; private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - private SkinType skinType = SkinType.BLUE_LIGHTGRAY; + private Skin skin = Skin.getFallbackInstance(); private boolean enabled = true; KeyboardPreviewDrawable( - Resources resources, KeyboardLayout keyboardLayout, int keyboardResourceId) { + Resources resources, KeyboardLayout keyboardLayout, KeyboardSpecification specification) { this.resources = resources; this.keyboardLayout = keyboardLayout; - this.keyboardResourceId = keyboardResourceId; + this.specification = specification; } @Override @@ -215,12 +217,13 @@ public void draw(Canvas canvas) { // Look up cache. BitmapCache cache = BitmapCache.getInstance(); - Bitmap bitmap = cache.get(keyboardLayout, bounds.width(), bounds.height(), skinType); + Bitmap bitmap = cache.get(keyboardLayout, bounds.width(), bounds.height(), skin); if (bitmap == null) { bitmap = createBitmap( - resources, keyboardResourceId, bounds.width(), bounds.height(), skinType); + resources, specification, bounds.width(), bounds.height(), + resources.getDimensionPixelSize(R.dimen.pref_inputstyle_reference_width), skin); if (bitmap != null) { - cache.put(keyboardLayout, skinType, bitmap); + cache.put(keyboardLayout, skin, bitmap); } } @@ -234,48 +237,48 @@ public void draw(Canvas canvas) { } } + /** + * @param width width of returned {@code Bitmap} + * @param height height of returned {@code Bitmap} + * @param virtualWidth virtual width of keyboard. This value is used when rendering. + * virtualHeight is internally calculated based on given arguments keeping aspect ratio. + */ + @Nullable private static Bitmap createBitmap( - Resources resources, int resourceId, int width, int height, SkinType skinType) { - Keyboard keyboard = getParsedKeyboard(resources, resourceId, width, height); + Resources resources, KeyboardSpecification specification, int width, int height, + int virtualWidth, Skin skin) { + Preconditions.checkNotNull(skin); + // Scaling is required because some icons are draw with specified fixed size. + float scale = width / (float) virtualWidth; + int virtualHeight = (int) (height / scale); + Keyboard keyboard = getParsedKeyboard(resources, specification, virtualWidth, virtualHeight); if (keyboard == null) { return null; } Bitmap bitmap = MozcUtil.createBitmap(width, height, Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); - DrawableCache drawableCache = new DrawableCache(new MozcDrawableFactory(resources)); - drawableCache.setSkinType(skinType); + canvas.scale(scale, scale); + DrawableCache drawableCache = new DrawableCache(resources); + drawableCache.setSkin(skin); // Fill background. { - Optional optionalKeyboardBackground = - drawableCache.getDrawable(skinType.windowBackgroundResourceId); - if (!optionalKeyboardBackground.isPresent()) { - // Default black drawing. - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setColor(0xFF000000); - canvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), paint); - } else { - Drawable keyboardBackground = optionalKeyboardBackground.get(); - if (keyboardBackground instanceof BitmapDrawable) { - // If the background is bitmap resource, set repeat mode. - BitmapDrawable.class.cast(keyboardBackground).setTileModeXY( - TileMode.REPEAT, TileMode.REPEAT); - } - keyboardBackground.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); - keyboardBackground.draw(canvas); - } + Drawable keyboardBackground = + skin.windowBackgroundDrawable.getConstantState().newDrawable(); + keyboardBackground.setBounds(0, 0, virtualWidth, virtualHeight); + keyboardBackground.draw(canvas); } // Draw keyboard layout. { BackgroundDrawableFactory backgroundDrawableFactory = - new BackgroundDrawableFactory(resources.getDisplayMetrics().density); - backgroundDrawableFactory.setSkinType(skinType); + new BackgroundDrawableFactory(resources); + backgroundDrawableFactory.setSkin(skin); KeyboardViewBackgroundSurface backgroundSurface = new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache); backgroundSurface.requestUpdateKeyboard(keyboard, Collections.emptySet()); - backgroundSurface.requestUpdateSize(bitmap.getWidth(), bitmap.getHeight()); + backgroundSurface.requestUpdateSize(virtualWidth, virtualHeight); backgroundSurface.update(); backgroundSurface.draw(canvas); backgroundSurface.reset(); // Release the background bitmap and its canvas. @@ -285,10 +288,11 @@ private static Bitmap createBitmap( } /** Create a Keyboard instance which fits the current bitmap. */ + @Nullable private static Keyboard getParsedKeyboard( - Resources resources, int resourceId, int width, int height) { + Resources resources, KeyboardSpecification specification, int width, int height) { KeyboardParser parser = new KeyboardParser( - resources, resources.getXml(resourceId), width, height); + resources, width, height, specification); try { return parser.parseKeyboard(); } catch (XmlPullParserException e) { @@ -300,8 +304,8 @@ private static Keyboard getParsedKeyboard( return null; } - void setSkinType(SkinType skinType) { - this.skinType = skinType; + void setSkin(Skin skin) { + this.skin = skin; invalidateSelf(); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/MiniBrowserActivity.java b/src/android/src/com/google/android/inputmethod/japanese/preference/MiniBrowserActivity.java index 6ee4c0ec3..304add196 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/MiniBrowserActivity.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/MiniBrowserActivity.java @@ -80,24 +80,32 @@ static final class MiniBrowserClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - Preconditions.checkNotNull(view); - Preconditions.checkNotNull(url); - - // Use temporary matcher intentionally. - // Regex engine is rather heavy to instantiate so use it as less as possible. - if (!Pattern.matches(restrictionPattern, url)) { - // If the URL's doesn't match restriction pattern, - // delegate the operation to the default browser. - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - if (!packageManager.queryIntentActivities(browserIntent, 0).isEmpty()) { - context.startActivity(browserIntent); + try { + Preconditions.checkNotNull(view); + Preconditions.checkNotNull(url); + + // Use temporary matcher intentionally. + // Regex engine is rather heavy to instantiate so use it as less as possible. + if (!Pattern.matches(restrictionPattern, url)) { + // If the URL's doesn't match restriction pattern, + // delegate the operation to the default browser. + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + if (!packageManager.queryIntentActivities(browserIntent, 0).isEmpty()) { + context.startActivity(browserIntent); + } + // If no default browser is available, do nothing. + return true; } - // If no default browser is available, do nothing. + // Prevent from invoking default browser. + // In some special environment default browser is not installed. + return false; + } catch (Throwable e) { + // This method might be called from native layer. + // Therefore throwing something from here causes native crash. + // To prevent from native crash, catches all here. + // At least SecurityException must be caught here for Android-TV. return true; } - // Prevent from invoking default browser. - // In some special environment default browser is not installed. - return false; } } @@ -115,6 +123,15 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(webView); } + @Override + protected void onPause() { + // Clear cache in order to show appropriate website even if system locale is changed. + if (this.webView.isPresent()) { + this.webView.get().clearCache(true); + } + super.onPause(); + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Preconditions.checkNotNull(event); diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/MozcBasePreferenceActivity.java b/src/android/src/com/google/android/inputmethod/japanese/preference/MozcBasePreferenceActivity.java index 2593853a0..75160c669 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/MozcBasePreferenceActivity.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/MozcBasePreferenceActivity.java @@ -37,6 +37,7 @@ import org.mozc.android.inputmethod.japanese.preference.KeyboardPreviewDrawable.BitmapCache; import org.mozc.android.inputmethod.japanese.preference.KeyboardPreviewDrawable.CacheReferenceKey; import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; @@ -155,12 +156,13 @@ protected void onPostResume() { @VisibleForTesting void onPostResumeInternal(ApplicationInitializer initializer) { Context context = getApplicationContext(); - boolean omitWelcomeActivity = false; Optional forwardIntent = initializer.initialize( - omitWelcomeActivity, + MozcUtil.isSystemApplication(context), MozcUtil.isDevChannel(context), DependencyFactory.getDependency(getApplicationContext()).isWelcomeActivityPreferrable(), - MozcUtil.getAbiIndependentVersionCode(context)); + MozcUtil.getAbiIndependentVersionCode(context), + LauncherIconManagerFactory.getDefaultInstance(), + PreferenceUtil.getDefaultPreferenceManagerStatic()); if (forwardIntent.isPresent()) { startActivity(forwardIntent.get()); } else { diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/MozcFragmentPreferenceActivity.java b/src/android/src/com/google/android/inputmethod/japanese/preference/MozcFragmentPreferenceActivity.java index b403565cb..873ae274b 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/MozcFragmentPreferenceActivity.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/MozcFragmentPreferenceActivity.java @@ -29,12 +29,46 @@ package org.mozc.android.inputmethod.japanese.preference; +import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.preference.PreferenceManager; + /** * Main Activity class for the fragment based preference UI on Android with API Level >= 11. * */ public class MozcFragmentPreferenceActivity extends MozcFragmentBasePreferenceActivity { + public MozcFragmentPreferenceActivity() { super(PreferencePage.FLAT); } + + private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = + new OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PreferenceUtil.PREF_LAUNCHER_ICON_VISIBILITY_KEY.equals(key)) { + LauncherIconManagerFactory.getDefaultInstance() + .updateLauncherIconVisibility(MozcFragmentPreferenceActivity.this); + } + } + }; + + @Override + protected void onResume() { + super.onResume(); + PreferenceManager + .getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); + } + + @Override + protected void onPause() { + PreferenceManager + .getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); + super.onPause(); + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/preference/PreferenceUtil.java b/src/android/src/com/google/android/inputmethod/japanese/preference/PreferenceUtil.java index 324c3caec..169d671d3 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/preference/PreferenceUtil.java +++ b/src/android/src/com/google/android/inputmethod/japanese/preference/PreferenceUtil.java @@ -33,6 +33,7 @@ import org.mozc.android.inputmethod.japanese.MozcUtil; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout; import org.mozc.android.inputmethod.japanese.resources.R; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import android.content.Context; @@ -45,6 +46,9 @@ import android.preference.PreferenceManager; import android.util.DisplayMetrics; +import java.util.HashSet; +import java.util.Set; + /** * Utilities for Mozc preferences. * @@ -56,7 +60,14 @@ interface PreferenceManagerInterface { public Preference findPreference(CharSequence key); } - private static class PreferenceManagerInterfaceImpl implements PreferenceManagerInterface { + /** Simple {@code PreferenceManager} wrapper for testing purpose. + * This interface wraps static method so no constructor is required. + */ + public interface PreferenceManagerStaticInterface { + public void setDefaultValues(Context context, int id, boolean readAgain); + } + + static class PreferenceManagerInterfaceImpl implements PreferenceManagerInterface { private final PreferenceManager preferenceManager; PreferenceManagerInterfaceImpl(PreferenceManager preferenceManager) { @@ -69,6 +80,24 @@ public Preference findPreference(CharSequence key) { } } + private static Optional defaultPreferenceManagerStatic = + Optional.absent(); + + public static PreferenceManagerStaticInterface getDefaultPreferenceManagerStatic() { + // As construction cost of defaultPrereferenceManagerStatic is cheap and it is invariant, + // no lock mechanism is employed here. + if (!defaultPreferenceManagerStatic.isPresent()) { + defaultPreferenceManagerStatic = Optional.of( + new PreferenceManagerStaticInterface() { + @Override + public void setDefaultValues(Context context, int id, boolean readAgain) { + PreferenceManager.setDefaultValues(context, id, readAgain); + } + }); + } + return defaultPreferenceManagerStatic.get(); + } + static class CurrentKeyboardLayoutPreferenceChangeListener implements OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { @@ -133,10 +162,10 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { "pref_portrait_fullscreen_key"; public static final String PREF_LANDSCAPE_FULLSCREEN_KEY = "pref_landscape_fullscreen_key"; - public static final String PREF_SKIN_TYPE = "pref_skin_type_key"; - public static final String PREF_HARDWARE_KEYMAP = "pref_hardware_keymap"; // Keys for generic preferences. + public static final String PREF_HARDWARE_KEYMAP = "pref_hardware_keymap"; + public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; public static final String PREF_HAPTIC_FEEDBACK_KEY = "pref_haptic_feedback_key"; public static final String PREF_HAPTIC_FEEDBACK_DURATION_KEY = "pref_haptic_feedback_duration_key"; @@ -159,6 +188,10 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { public static final String PREF_OTHER_INCOGNITO_MODE_KEY = "pref_other_anonimous_mode_key"; public static final String PREF_OTHER_USAGE_STATS_KEY = "pref_other_usage_stats_key"; public static final String PREF_ABOUT_VERSION = "pref_about_version"; + public static final String PREF_LAUNCHER_ICON_VISIBILITY_KEY = "pref_launcher_icon_visibility"; + // Application lifecycle + public static final String PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE = + "pref_last_launch_abi_independent_version_code"; private static final OnPreferenceChangeListener CURRENT_KEYBOARD_LAYOUT_PREFERENCE_CHANGE_LISTENER = @@ -168,8 +201,8 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { private PreferenceUtil() { } - static boolean isLandscapeKeyboardSettingActive(SharedPreferences sharedPreferences, - int deviceOrientation) { + public static boolean isLandscapeKeyboardSettingActive( + SharedPreferences sharedPreferences, int deviceOrientation) { Preconditions.checkNotNull(sharedPreferences); if (sharedPreferences.getBoolean(PREF_USE_PORTRAIT_KEYBOARD_SETTINGS_FOR_LANDSCAPE_KEY, true)) { // Always use portrait configuration. @@ -340,4 +373,25 @@ public static > T getEnum( SharedPreferences sharedPreference, String key, Class type, T defaultValue) { return getEnum(sharedPreference, key, type, defaultValue, defaultValue); } + + public static void setDefaultValues(PreferenceManagerStaticInterface preferenceManager, + Context context, boolean isDebug, boolean useUsageStats) { + Preconditions.checkNotNull(preferenceManager); + Preconditions.checkNotNull(context); + Set preferenceResources = new HashSet(); + // Collect all the preferences resource ids from possible preference pages, + // removing duplication. + for (PreferencePage page : PreferencePage.values()) { + for (int id : PreferencePage.getResourceIdList(page, isDebug, useUsageStats)) { + preferenceResources.add(id); + } + } + for (int id : preferenceResources) { + // 'true' here means the preferences which have not set yet are *always* set here. + // This doesn't mean *Reset all the preferences*. + // (if 'false' the process will be done once on the first launch + // so even if new preferences are added their default values will not be set here) + preferenceManager.setDefaultValues(context, id, true); + } + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/session/LocalSessionHandler.java b/src/android/src/com/google/android/inputmethod/japanese/session/LocalSessionHandler.java index ccd1a3bee..4cb0c15da 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/session/LocalSessionHandler.java +++ b/src/android/src/com/google/android/inputmethod/japanese/session/LocalSessionHandler.java @@ -32,6 +32,7 @@ import org.mozc.android.inputmethod.japanese.MozcLog; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; import org.mozc.android.inputmethod.japanese.util.ZipFileUtil; +import com.google.common.base.Preconditions; import com.google.protobuf.InvalidProtocolBufferException; import android.content.Context; @@ -50,6 +51,7 @@ * */ class LocalSessionHandler implements SessionHandler { + private static final String USER_PROFILE_DIRECTORY_NAME = ".mozc"; // The file name of the system dictionary and connection data file in the apk. // This is determined in build.xml. @@ -60,7 +62,7 @@ class LocalSessionHandler implements SessionHandler { @Override public void initialize(Context context) { try { - ApplicationInfo info = context.getApplicationInfo(); + ApplicationInfo info = Preconditions.checkNotNull(context).getApplicationInfo(); // Ensure the user profile directory exists. File userProfileDirectory = new File(info.dataDir, USER_PROFILE_DIRECTORY_NAME); @@ -104,7 +106,7 @@ public void initialize(Context context) { @Override public Command evalCommand(Command command) { - byte[] inBytes = command.toByteArray(); + byte[] inBytes = Preconditions.checkNotNull(command).toByteArray(); byte[] outBytes = null; outBytes = MozcJNI.evalCommand(inBytes); try { diff --git a/src/android/src/com/google/android/inputmethod/japanese/session/MozcCommandDebugger.java b/src/android/src/com/google/android/inputmethod/japanese/session/MozcCommandDebugger.java index 802b1ce1f..d56dd8f6a 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/session/MozcCommandDebugger.java +++ b/src/android/src/com/google/android/inputmethod/japanese/session/MozcCommandDebugger.java @@ -31,6 +31,7 @@ import org.mozc.android.inputmethod.japanese.MozcLog; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; +import com.google.common.base.Preconditions; /** * This class logs only input/output protocol buffer Command for MozcJNI. @@ -44,11 +45,11 @@ private MozcCommandDebugger() { static void inLog(Command inCommand) { MozcLog.v(""); - MozcLog.v(inCommand.getInput().toString()); + MozcLog.v(Preconditions.checkNotNull(inCommand).getInput().toString()); } static void outLog(Command outCommand) { - MozcLog.v(outCommand.getOutput().toString()); + MozcLog.v(Preconditions.checkNotNull(outCommand).getOutput().toString()); MozcLog.v(""); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/session/MozcJNI.java b/src/android/src/com/google/android/inputmethod/japanese/session/MozcJNI.java index 307326e8b..1e29c1f70 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/session/MozcJNI.java +++ b/src/android/src/com/google/android/inputmethod/japanese/session/MozcJNI.java @@ -30,6 +30,7 @@ package org.mozc.android.inputmethod.japanese.session; import org.mozc.android.inputmethod.japanese.MozcLog; +import com.google.common.base.Preconditions; import java.nio.Buffer; @@ -53,6 +54,11 @@ class MozcJNI { static void load( String userProfileDirectoryPath, Buffer dictionaryBuffer, Buffer connectionDataBuffer, String expectedVersion) { + Preconditions.checkNotNull(userProfileDirectoryPath); + Preconditions.checkNotNull(dictionaryBuffer); + Preconditions.checkNotNull(connectionDataBuffer); + Preconditions.checkNotNull(expectedVersion); + if (isLoaded) { return; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/session/SessionExecutor.java b/src/android/src/com/google/android/inputmethod/japanese/session/SessionExecutor.java index bfee8be51..07b3c2999 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/session/SessionExecutor.java +++ b/src/android/src/com/google/android/inputmethod/japanese/session/SessionExecutor.java @@ -31,6 +31,7 @@ import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; import org.mozc.android.inputmethod.japanese.MozcLog; +import org.mozc.android.inputmethod.japanese.MozcUtil; import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Capability; @@ -60,6 +61,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -83,27 +85,29 @@ * */ public class SessionExecutor { + // At the moment we call mozc server via JNI interface directly, // while other platforms, e.g. Win/Mac/Linux etc, use IPC. // In order to keep the call order correctly, we call it from the single worker thread. // Note that we use well-known double check lazy initialization, // so that we can inject instances via reflections for testing purposes. - @VisibleForTesting static volatile SessionExecutor instance; + @VisibleForTesting static volatile Optional instance = Optional.absent(); + private static SessionExecutor getInstanceInternal( - SessionHandlerFactory factory, Context applicationContext) { - SessionExecutor result = instance; - if (result == null) { + Optional factory, Context applicationContext) { + Optional result = instance; + if (!result.isPresent()) { synchronized (SessionExecutor.class) { result = instance; - if (result == null) { - result = instance = new SessionExecutor(); - if (factory != null) { - result.reset(factory, applicationContext); + if (!result.isPresent()) { + result = instance = Optional.of(new SessionExecutor()); + if (factory.isPresent()) { + result.get().reset(factory.get(), applicationContext); } } } } - return result; + return result.get(); } /** @@ -112,10 +116,11 @@ private static SessionExecutor getInstanceInternal( * @param executor the new instance to replace the old one. * @return the old instance. */ - public static SessionExecutor setInstanceForTest(SessionExecutor executor) { + @VisibleForTesting public static Optional setInstanceForTest( + Optional executor) { synchronized (SessionExecutor.class) { - SessionExecutor old = instance; - instance = executor; + Optional old = instance; + instance = Preconditions.checkNotNull(executor); return old; } } @@ -126,7 +131,8 @@ public static SessionExecutor setInstanceForTest(SessionExecutor executor) { * by client in appropriate timing. */ public static SessionExecutor getInstance(Context applicationContext) { - return getInstanceInternal(null, applicationContext); + return getInstanceInternal( + Optional.absent(), Preconditions.checkNotNull(applicationContext)); } /** @@ -135,81 +141,84 @@ public static SessionExecutor getInstance(Context applicationContext) { */ public static SessionExecutor getInstanceInitializedIfNecessary( SessionHandlerFactory factory, Context applicationContext) { - return getInstanceInternal(factory, applicationContext); + return getInstanceInternal( + Optional.of(factory), Preconditions.checkNotNull(applicationContext)); } - private static volatile HandlerThread sessionHandlerThread; + private static volatile Optional sessionHandlerThread = Optional.absent(); private static HandlerThread getHandlerThread() { - HandlerThread result = sessionHandlerThread; - if (result == null) { + Optional result = sessionHandlerThread; + if (!result.isPresent()) { synchronized (SessionExecutor.class) { result = sessionHandlerThread; - if (result == null) { - result = new HandlerThread("Session worker thread"); - result.setDaemon(true); - result.start(); + if (!result.isPresent()) { + result = Optional.of(new HandlerThread("Session worker thread")); + result.get().setDaemon(true); + result.get().start(); sessionHandlerThread = result; } } } - return result; + return result.get(); } /** * An interface to accept the result of asynchronous evaluation. */ public interface EvaluationCallback { - void onCompleted(Command command, KeyEventInterface triggeringKeyEvent); + void onCompleted(Optional command, Optional triggeringKeyEvent); } - // Context class during each evaluation. Package private for testing purpose. - static class AsynchronousEvaluationContext { + @VisibleForTesting static class AsynchronousEvaluationContext { + // For asynchronous evaluation, we set the sessionId in the callback running on the worker // thread. So, this class has Input.Builer as an argument for an evaluation, while // SynchronousEvaluationContext has Input because it is not necessary to be set sessionId. final long timeStamp; final Input.Builder inputBuilder; - volatile Command outCommand; - final KeyEventInterface triggeringKeyEvent; - final EvaluationCallback callback; - final Handler callbackHandler; + volatile Optional outCommand = Optional.absent(); + final Optional triggeringKeyEvent; + final Optional callback; + final Optional callbackHandler; AsynchronousEvaluationContext(long timeStamp, Input.Builder inputBuilder, - KeyEventInterface triggeringKeyEvent, - EvaluationCallback callback, - Handler callbackHandler) { + Optional triggeringKeyEvent, + Optional callback, + Optional callbackHandler) { this.timeStamp = timeStamp; - this.inputBuilder = inputBuilder; - this.triggeringKeyEvent = triggeringKeyEvent; - this.callback = callback; - this.callbackHandler = callbackHandler; + this.inputBuilder = Preconditions.checkNotNull(inputBuilder); + this.triggeringKeyEvent = Preconditions.checkNotNull(triggeringKeyEvent); + this.callback = Preconditions.checkNotNull(callback); + this.callbackHandler = Preconditions.checkNotNull(callbackHandler); } } - static class SynchronousEvaluationContext { + @VisibleForTesting static class SynchronousEvaluationContext { + final Input input; - volatile Command outCommand; + volatile Optional outCommand = Optional.absent(); final CountDownLatch evaluationSynchronizer; SynchronousEvaluationContext(Input input, CountDownLatch evaluationSynchronizer) { - this.input = input; - this.evaluationSynchronizer = evaluationSynchronizer; + this.input = Preconditions.checkNotNull(input); + this.evaluationSynchronizer = Preconditions.checkNotNull(evaluationSynchronizer); } } - // Context class just lines handler queue. - static class KeyEventCallBackContext { + /** Context class just lines handler queue. */ + @VisibleForTesting static class KeyEventCallbackContext { + final KeyEventInterface triggeringKeyEvent; final EvaluationCallback callback; final Handler callbackHandler; - KeyEventCallBackContext(KeyEventInterface triggeringKeyEvent, + KeyEventCallbackContext(KeyEventInterface triggeringKeyEvent, EvaluationCallback callback, Handler callbackHandler) { - this.triggeringKeyEvent = triggeringKeyEvent; - this.callback = callback; - this.callbackHandler = callbackHandler; + this.triggeringKeyEvent = Preconditions.checkNotNull(triggeringKeyEvent); + this.callback = Preconditions.checkNotNull(callback); + this.callbackHandler = Preconditions.checkNotNull(callbackHandler); } } @@ -222,9 +231,8 @@ static class KeyEventCallBackContext { * the UI thread if necessary. * All evaluations should be done with this class in order to keep evaluation in the incoming * order. - * Package private for testing purpose. */ - static class ExecutorMainCallback implements Handler.Callback { + @VisibleForTesting static class ExecutorMainCallback implements Handler.Callback { /** * Initializes the currently connected sesion handler. @@ -292,20 +300,19 @@ static class ExecutorMainCallback implements Handler.Callback { // Mozc session's ID. // Set on CREATE_SESSION and will not be updated. @VisibleForTesting long sessionId = INVALID_SESSION_ID; - @VisibleForTesting Request.Builder request; + @VisibleForTesting Optional request = Optional.absent(); // The logging for debugging is disabled by default. boolean isLogging = false; - ExecutorMainCallback(SessionHandler sessionHandler) { - if (sessionHandler == null) { - throw new NullPointerException("sessionHandler is null."); - } - this.sessionHandler = sessionHandler; + @VisibleForTesting ExecutorMainCallback(SessionHandler sessionHandler) { + this.sessionHandler = Preconditions.checkNotNull(sessionHandler); } @Override public boolean handleMessage(Message message) { + Preconditions.checkNotNull(message); + // Dispatch the message. switch (message.what) { case INITIALIZE_SESSION_HANDLER: @@ -322,12 +329,11 @@ public boolean handleMessage(Message message) { case EVALUATE_SYNCHRONOUSLY: evaluateSynchronously(SynchronousEvaluationContext.class.cast(message.obj)); break; - case UPDATE_REQUEST: { + case UPDATE_REQUEST: updateRequest(Input.Builder.class.cast(message.obj)); break; - } case PASS_TO_CALLBACK: - passToCallBack(KeyEventCallBackContext.class.cast(message.obj)); + passToCallBack(KeyEventCallbackContext.class.cast(message.obj)); break; default: // We don't process unknown messages. @@ -353,7 +359,7 @@ private Command evaluate(Input input) { return outCommand; } - void ensureSession() { + @VisibleForTesting void ensureSession() { if (sessionId != INVALID_SESSION_ID) { return; } @@ -370,15 +376,13 @@ void ensureSession() { // with ignoring its result. // Set mobile dedicated fields, which will not be changed. // Other fields may be set when the input view is changed. - request = Request.newBuilder() - .setMixedConversion(true) - .setZeroQuerySuggestion(true) - .setUpdateInputModeFromSurroundingText(false) - .setAutoPartialSuggestion(true); + Request.Builder builder = Request.newBuilder(); + MozcUtil.setSoftwareKeyboardRequest(builder); + request = Optional.of(builder); evaluate(Input.newBuilder() .setId(sessionId) .setType(CommandType.SET_REQUEST) - .setRequest(request) + .setRequest(request.get()) .build()); } @@ -393,7 +397,7 @@ void deleteSession() { .build(); evaluate(input); sessionId = INVALID_SESSION_ID; - request = null; + request = Optional.absent(); } /** @@ -407,7 +411,7 @@ void deleteSession() { * result). Squashing will happen when an output consists from only overwritable fields and * then another output, which will overwrite the UI change caused by the older output, comes. */ - static boolean isSquashableOutput(Output output) { + @VisibleForTesting static boolean isSquashableOutput(Output output) { // - If the output contains a result, it is not squashable because it is necessary // to commit the string to the client application. // - If it has deletion_range, it is not squashable either because it is necessary @@ -429,22 +433,22 @@ static boolean isSquashableOutput(Output output) { * @return {@code true} if the given {@code inputBuilder} needs to be set session id. * Otherwise {@code false}. */ - static boolean isSessionIdRequired(InputOrBuilder input) { + @VisibleForTesting static boolean isSessionIdRequired(InputOrBuilder input) { return !SESSION_INDEPENDENT_COMMAND_TYPE_SET.contains(input.getType()); } - void evaluateAsynchronously(AsynchronousEvaluationContext context, - Handler sessionExecutorHandler) { + @VisibleForTesting void evaluateAsynchronously(AsynchronousEvaluationContext context, + Handler sessionExecutorHandler) { // Before the evaluation, we remove all pending squashable result callbacks for performance // reason of less powerful devices. Input.Builder inputBuilder = context.inputBuilder; - Handler callbackHandler = context.callbackHandler; - if (callbackHandler != null && - inputBuilder.getCommand().getType() != SessionCommand.CommandType.EXPAND_SUGGESTION) { + Optional callbackHandler = context.callbackHandler; + if (callbackHandler.isPresent() + && inputBuilder.getCommand().getType() != SessionCommand.CommandType.EXPAND_SUGGESTION) { // Do not squash by EXPAND_SUGGESTION request, because the result of EXPAND_SUGGESTION // won't affect the inputConnection in MozcService, as the result should update // only candidates conceptually. - callbackHandler.removeMessages(CallbackHandler.SQUASHABLE_OUTPUT); + callbackHandler.get().removeMessages(CallbackHandler.SQUASHABLE_OUTPUT); } if (inputBuilder.hasKey() && @@ -463,51 +467,50 @@ void evaluateAsynchronously(AsynchronousEvaluationContext context, ensureSession(); inputBuilder.setId(sessionId); } - context.outCommand = evaluate(inputBuilder.build()); + context.outCommand = Optional.of(evaluate(inputBuilder.build())); // Invoke callback handler if necessary. - if (callbackHandler != null) { + if (callbackHandler.isPresent()) { // For performance reason of, especially, less powerful devices, we want to skip // rendering whose effect will be overwritten by following (pending) rendering. // We annotate if the output can be overwritten or not here, so that we can remove // only those messages in later evaluation. - Output output = context.outCommand.getOutput(); - Message message = callbackHandler.obtainMessage( + Output output = context.outCommand.get().getOutput(); + Message message = callbackHandler.get().obtainMessage( isSquashableOutput(output) ? CallbackHandler.SQUASHABLE_OUTPUT : CallbackHandler.UNSQUASHABLE_OUTPUT, context); - callbackHandler.sendMessage(message); + callbackHandler.get().sendMessage(message); } } - void evaluateSynchronously(SynchronousEvaluationContext context) { + @VisibleForTesting void evaluateSynchronously(SynchronousEvaluationContext context) { Input input = context.input; - if (isSessionIdRequired(input)) { - // We expect only non-session-id-related input for synchronous evaluation. - throw new IllegalArgumentException( - "session id is required in evaluateSynchronously, but we don't expect it: " + input); - } + Preconditions.checkArgument( + !isSessionIdRequired(input), + "We expect only non-session-id-related input for synchronous evaluation: " + input); - context.outCommand = evaluate(input); + context.outCommand = Optional.of(evaluate(input)); // The client thread is waiting for the evaluation by evaluationSynchronizer, // so notify the thread via the lock. context.evaluationSynchronizer.countDown(); } - void updateRequest(Input.Builder inputBuilder) { + @VisibleForTesting void updateRequest(Input.Builder inputBuilder) { ensureSession(); - request.mergeFrom(inputBuilder.getRequest()); + Preconditions.checkState(request.isPresent()); + request.get().mergeFrom(inputBuilder.getRequest()); Input input = inputBuilder .setId(sessionId) .setType(CommandType.SET_REQUEST) - .setRequest(request) + .setRequest(request.get()) .build(); // Do not render the result because the result does not have preedit. evaluate(input); } - void passToCallBack(KeyEventCallBackContext context) { + @VisibleForTesting void passToCallBack(KeyEventCallbackContext context) { Handler callbackHandler = context.callbackHandler; Message message = callbackHandler.obtainMessage(CallbackHandler.UNSQUASHABLE_OUTPUT, context); callbackHandler.sendMessage(message); @@ -517,7 +520,7 @@ void passToCallBack(KeyEventCallBackContext context) { /** * A handler to process callback for asynchronous evaluation on the UI thread. */ - static class CallbackHandler extends Handler { + @VisibleForTesting static class CallbackHandler extends Handler { /** * The message with this {@code what} cannot be overwritten by following evaluation. */ @@ -536,30 +539,28 @@ static class CallbackHandler extends Handler { @Override public void handleMessage(Message message) { - if (message.obj.getClass() == KeyEventCallBackContext.class) { - KeyEventCallBackContext keyEventContext = KeyEventCallBackContext.class.cast(message.obj); - keyEventContext.callback.onCompleted(null, keyEventContext.triggeringKeyEvent); + if (Preconditions.checkNotNull(message).obj.getClass() == KeyEventCallbackContext.class) { + KeyEventCallbackContext keyEventContext = KeyEventCallbackContext.class.cast(message.obj); + keyEventContext.callback.onCompleted( + Optional.absent(), Optional.of(keyEventContext.triggeringKeyEvent)); return; } - AsynchronousEvaluationContext context = - AsynchronousEvaluationContext.class.cast(message.obj); + AsynchronousEvaluationContext context = AsynchronousEvaluationContext.class.cast(message.obj); // Note that this method should be run on the UI thread, where removePendingEvaluations runs, // so we don't need to take a lock here. if (context.timeStamp - cancelTimeStamp > 0) { - context.callback.onCompleted(context.outCommand, context.triggeringKeyEvent); + Preconditions.checkState(context.callback.isPresent()); + context.callback.get().onCompleted(context.outCommand, context.triggeringKeyEvent); } } } - @VisibleForTesting Handler handler; - private ExecutorMainCallback mainCallback; + @VisibleForTesting Optional handler = Optional.absent(); + private Optional mainCallback = Optional.absent(); private final CallbackHandler callbackHandler; // Note that theoretically the constructor should be private in order to keep this singleton, - // but we use protected for testing purpose. - protected SessionExecutor() { - handler = null; - mainCallback = null; + @VisibleForTesting protected SessionExecutor() { callbackHandler = new CallbackHandler(Looper.getMainLooper()); } @@ -583,7 +584,8 @@ public void run() { */ @VisibleForTesting public void waitForAllQueuesForEmpty() { - waitForQueueForEmpty(handler); + Preconditions.checkState(handler.isPresent()); + waitForQueueForEmpty(handler.get()); waitForQueueForEmpty(callbackHandler); } @@ -591,19 +593,21 @@ public void waitForAllQueuesForEmpty() { * Resets the instance by setting {@code SessionHandler} created by the given {@code factory}. */ public void reset(SessionHandlerFactory factory, Context applicationContext) { + Preconditions.checkNotNull(factory); + Preconditions.checkNotNull(applicationContext); HandlerThread thread = getHandlerThread(); - mainCallback = new ExecutorMainCallback(factory.create()); - handler = new Handler(thread.getLooper(), mainCallback); - handler.sendMessage(handler.obtainMessage(ExecutorMainCallback.INITIALIZE_SESSION_HANDLER, - applicationContext)); + mainCallback = Optional.of(new ExecutorMainCallback(factory.create())); + handler = Optional.of(new Handler(thread.getLooper(), mainCallback.get())); + handler.get().sendMessage(handler.get().obtainMessage( + ExecutorMainCallback.INITIALIZE_SESSION_HANDLER, applicationContext)); } /** * @param isLogging Set {@code true} if logging of evaluations is needed. */ public void setLogging(boolean isLogging) { - if (mainCallback != null) { - mainCallback.isLogging = isLogging; + if (mainCallback.isPresent()) { + mainCallback.get().isLogging = isLogging; } } @@ -612,18 +616,19 @@ public void setLogging(boolean isLogging) { */ public void removePendingEvaluations() { callbackHandler.cancelTimeStamp = System.nanoTime(); - if (handler != null) { - handler.removeMessages(ExecutorMainCallback.EVALUATE_ASYNCHRONOUSLY); - handler.removeMessages(ExecutorMainCallback.EVALUATE_KEYEVENT_ASYNCHRONOUSLY); - handler.removeMessages(ExecutorMainCallback.EVALUATE_SYNCHRONOUSLY); - handler.removeMessages(ExecutorMainCallback.UPDATE_REQUEST); + if (handler.isPresent()) { + handler.get().removeMessages(ExecutorMainCallback.EVALUATE_ASYNCHRONOUSLY); + handler.get().removeMessages(ExecutorMainCallback.EVALUATE_KEYEVENT_ASYNCHRONOUSLY); + handler.get().removeMessages(ExecutorMainCallback.EVALUATE_SYNCHRONOUSLY); + handler.get().removeMessages(ExecutorMainCallback.UPDATE_REQUEST); } callbackHandler.removeMessages(CallbackHandler.UNSQUASHABLE_OUTPUT); callbackHandler.removeMessages(CallbackHandler.SQUASHABLE_OUTPUT); } public void deleteSession() { - handler.sendMessage(handler.obtainMessage(ExecutorMainCallback.DELETE_SESSION)); + Preconditions.checkState(handler.isPresent()); + handler.get().sendMessage(handler.get().obtainMessage(ExecutorMainCallback.DELETE_SESSION)); } /** @@ -634,71 +639,84 @@ public void deleteSession() { * This method returns immediately, i.e., even after this method's invocation, * it shouldn't be assumed that the evaluation is done. * - * Note that this method is exposed as package private for testing purpose. - * * @param inputBuilder the input data * @param triggeringKeyEvent a key event which triggers this evaluation * @param callback a callback handler if needed */ - void evaluateAsynchronously(Input.Builder inputBuilder, KeyEventInterface triggeringKeyEvent, - EvaluationCallback callback) { + @VisibleForTesting void evaluateAsynchronously( + Input.Builder inputBuilder, Optional triggeringKeyEvent, + Optional callback) { + Preconditions.checkState(handler.isPresent()); AsynchronousEvaluationContext context = new AsynchronousEvaluationContext( - System.nanoTime(), inputBuilder, triggeringKeyEvent, - callback, callback == null ? null : callbackHandler); - int type = (triggeringKeyEvent != null) + System.nanoTime(), Preconditions.checkNotNull(inputBuilder), + Preconditions.checkNotNull(triggeringKeyEvent), Preconditions.checkNotNull(callback), + callback.isPresent() ? Optional.of(callbackHandler) : Optional.absent()); + int type = (triggeringKeyEvent.isPresent()) ? ExecutorMainCallback.EVALUATE_KEYEVENT_ASYNCHRONOUSLY : ExecutorMainCallback.EVALUATE_ASYNCHRONOUSLY; - handler.sendMessage(handler.obtainMessage(type, context)); + handler.get().sendMessage(handler.get().obtainMessage(type, context)); } /** * Sends {@code SEND_KEY} command to the server asynchronously. */ public void sendKey(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface triggeringKeyEvent, - List touchEventList, EvaluationCallback callback) { + List touchEventList, EvaluationCallback callback) { + Preconditions.checkNotNull(mozcKeyEvent); + Preconditions.checkNotNull(triggeringKeyEvent); + Preconditions.checkNotNull(touchEventList); + Preconditions.checkNotNull(callback); + Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_KEY) .setKey(mozcKeyEvent) .addAllTouchEvents(touchEventList); - evaluateAsynchronously(inputBuilder, triggeringKeyEvent, callback); + evaluateAsynchronously(inputBuilder, Optional.of(triggeringKeyEvent), Optional.of(callback)); } /** * Sends {@code SUBMIT} command to the server asynchronously. */ public void submit(EvaluationCallback callback) { + Preconditions.checkNotNull(callback); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(SessionCommand.CommandType.SUBMIT)); - evaluateAsynchronously(inputBuilder, null, callback); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); } /** * Sends {@code SWITCH_INPUT_MODE} command to the server asynchronously. */ - public void switchInputMode(KeyEventInterface triggeringKeyEvent, CompositionMode mode, + public void switchInputMode(Optional triggeringKeyEvent, CompositionMode mode, EvaluationCallback callback) { + Preconditions.checkNotNull(triggeringKeyEvent); + Preconditions.checkNotNull(mode); + Preconditions.checkNotNull(callback); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(SessionCommand.CommandType.SWITCH_INPUT_MODE) .setCompositionMode(mode)); - evaluateAsynchronously(inputBuilder, triggeringKeyEvent, callback); + evaluateAsynchronously(inputBuilder, triggeringKeyEvent, Optional.of(callback)); } /** * Sends {@code SUBMIT_CANDIDATE} command to the server asynchronously. */ public void submitCandidate(int candidateId, Optional rowIndex, - EvaluationCallback callback) { + EvaluationCallback callback) { Preconditions.checkNotNull(rowIndex); + Preconditions.checkNotNull(callback); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(SessionCommand.CommandType.SUBMIT_CANDIDATE) .setId(candidateId)); - evaluateAsynchronously(inputBuilder, null, callback); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); if (rowIndex.isPresent()) { candidateSubmissionStatsEvent(rowIndex.get()); @@ -753,45 +771,78 @@ public void resetContext() { .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(SessionCommand.CommandType.RESET_CONTEXT)); - evaluateAsynchronously(inputBuilder, null, null); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.absent()); } /** * Sends {@code MOVE_CURSOR} command to the server asynchronously. */ public void moveCursor(int cursorPosition, EvaluationCallback callback) { + Preconditions.checkNotNull(callback); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(ProtoCommands.SessionCommand.CommandType.MOVE_CURSOR) .setCursorPosition(cursorPosition)); - evaluateAsynchronously(inputBuilder, null, callback); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); + } + + /** + * Sends {@code CONVERT_NEXT_PAGE} command to the server asynchronously. + */ + public void pageDown(EvaluationCallback callback) { + Preconditions.checkNotNull(callback); + Input.Builder inputBuilder = Input.newBuilder() + .setType(CommandType.SEND_COMMAND) + .setCommand(SessionCommand.newBuilder() + .setType(SessionCommand.CommandType.CONVERT_NEXT_PAGE)); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); + } + + /** + * Sends {@code CONVERT_PREV_PAGE} command to the server asynchronously. + */ + public void pageUp(EvaluationCallback callback) { + Preconditions.checkNotNull(callback); + Input.Builder inputBuilder = Input.newBuilder() + .setType(CommandType.SEND_COMMAND) + .setCommand(SessionCommand.newBuilder() + .setType(SessionCommand.CommandType.CONVERT_PREV_PAGE)); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); } /** * Sends {@code SWITCH_INPUT_FIELD_TYPE} command to the server asynchronously. */ public void switchInputFieldType(InputFieldType inputFieldType) { + Preconditions.checkNotNull(inputFieldType); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(ProtoCommands.SessionCommand.CommandType.SWITCH_INPUT_FIELD_TYPE)) .setContext(ProtoCommands.Context.newBuilder() .setInputFieldType(inputFieldType)); - evaluateAsynchronously(inputBuilder, null, null); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.absent()); } /** * Sends {@code UNDO_OR_REWIND} command to the server asynchronously. */ - public void undoOrRewind(List touchEventList, - EvaluationCallback callback) { + public void undoOrRewind(List touchEventList, EvaluationCallback callback) { + Preconditions.checkNotNull(touchEventList); + Preconditions.checkNotNull(callback); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand(SessionCommand.newBuilder() .setType(SessionCommand.CommandType.UNDO_OR_REWIND)) .addAllTouchEvents(touchEventList); - evaluateAsynchronously(inputBuilder, null, callback); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); } /** @@ -799,28 +850,36 @@ public void undoOrRewind(List touchEventList, * to the server asynchronously. */ public void insertToStorage(StorageType type, String key, List values) { + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(values); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.INSERT_TO_STORAGE) .setStorageEntry(GenericStorageEntry.newBuilder() .setType(type) .setKey(key) .addAllValue(values)); - evaluateAsynchronously(inputBuilder, null, null); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.absent()); } public void expandSuggestion(EvaluationCallback callback) { + Preconditions.checkNotNull(callback); Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SEND_COMMAND) .setCommand( SessionCommand.newBuilder().setType(SessionCommand.CommandType.EXPAND_SUGGESTION)); - evaluateAsynchronously(inputBuilder, null, callback); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.of(callback)); } - public void preferenceUsageStatsEvent(SharedPreferences sharedPreferences) { + public void preferenceUsageStatsEvent(SharedPreferences sharedPreferences, Resources resources) { Preconditions.checkNotNull(sharedPreferences); + Preconditions.checkNotNull(resources); ClientSidePreference landscapePreference = - new ClientSidePreference(sharedPreferences, Configuration.ORIENTATION_LANDSCAPE); + new ClientSidePreference( + sharedPreferences, resources, Configuration.ORIENTATION_LANDSCAPE); evaluateAsynchronously( Input.newBuilder() .setType(CommandType.SEND_COMMAND) @@ -828,10 +887,11 @@ public void preferenceUsageStatsEvent(SharedPreferences sharedPreferences) { .setType(SessionCommand.CommandType.USAGE_STATS_EVENT) .setUsageStatsEvent(UsageStatsEvent.SOFTWARE_KEYBOARD_LAYOUT_LANDSCAPE) .setUsageStatsEventIntValue(landscapePreference.getKeyboardLayout().getId())), - null, null); + Optional.absent(), Optional.absent()); ClientSidePreference portraitPreference = - new ClientSidePreference(sharedPreferences, Configuration.ORIENTATION_PORTRAIT); + new ClientSidePreference( + sharedPreferences, resources, Configuration.ORIENTATION_PORTRAIT); evaluateAsynchronously( Input.newBuilder() .setType(CommandType.SEND_COMMAND) @@ -839,10 +899,10 @@ public void preferenceUsageStatsEvent(SharedPreferences sharedPreferences) { .setType(SessionCommand.CommandType.USAGE_STATS_EVENT) .setUsageStatsEvent(UsageStatsEvent.SOFTWARE_KEYBOARD_LAYOUT_PORTRAIT) .setUsageStatsEventIntValue(portraitPreference.getKeyboardLayout().getId())), - null, null); + Optional.absent(), Optional.absent()); } - public void touchEventUsageStatsEvent(List touchEventList) { + public void touchEventUsageStatsEvent(List touchEventList) { if (Preconditions.checkNotNull(touchEventList).isEmpty()) { return; } @@ -852,26 +912,27 @@ public void touchEventUsageStatsEvent(List touchEventList) .setCommand(SessionCommand.newBuilder() .setType(SessionCommand.CommandType.USAGE_STATS_EVENT)) .addAllTouchEvents(touchEventList); - evaluateAsynchronously(inputBuilder, null, null); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.absent()); } public void syncData() { Input.Builder inputBuilder = Input.newBuilder() .setType(CommandType.SYNC_DATA); - evaluateAsynchronously(inputBuilder, null, null); + evaluateAsynchronously( + inputBuilder, Optional.absent(), Optional.absent()); } /** * Evaluates the input on the JNI worker thread, and wait that the evaluation is done. * This method blocks (typically <30ms). - * - * Note that this method is exposed as package private for testing purpose. */ - Output evaluateSynchronously(Input input) { + @VisibleForTesting Output evaluateSynchronously(Input input) { + Preconditions.checkState(handler.isPresent()); CountDownLatch evaluationSynchronizer = new CountDownLatch(1); SynchronousEvaluationContext context = new SynchronousEvaluationContext(input, evaluationSynchronizer); - handler.sendMessage(handler.obtainMessage( + handler.get().sendMessage(handler.get().obtainMessage( ExecutorMainCallback.EVALUATE_SYNCHRONOUSLY, context)); try { @@ -880,7 +941,7 @@ Output evaluateSynchronously(Input input) { MozcLog.w("Session thread is interrupted during evaluation."); } - return context.outCommand.getOutput(); + return context.outCommand.get().getOutput(); } /** @@ -895,19 +956,22 @@ public Config getConfig() { * Sets the given {@code config} to the server. */ public void setConfig(Config config) { + Preconditions.checkNotNull(config); // Ignore output. evaluateAsynchronously( - Input.newBuilder().setType(Input.CommandType.SET_CONFIG).setConfig(config), null, null); + Input.newBuilder().setType(Input.CommandType.SET_CONFIG).setConfig(config), + Optional.absent(), Optional.absent()); } /** * Sets the given {@code config} to the server as imposed config. */ public void setImposedConfig(Config config) { + Preconditions.checkNotNull(config); // Ignore output. evaluateAsynchronously( Input.newBuilder().setType(Input.CommandType.SET_IMPOSED_CONFIG).setConfig(config), - null, null); + Optional.absent(), Optional.absent()); } /** @@ -939,6 +1003,7 @@ public void clearUserPrediction() { * @param storageType the storage to be cleared */ public void clearStorage(StorageType storageType) { + Preconditions.checkNotNull(storageType); evaluateSynchronously( Input.newBuilder() .setType(CommandType.CLEAR_STORAGE) @@ -950,6 +1015,7 @@ public void clearStorage(StorageType storageType) { * Reads stored values of the given {@code type} from the server, and returns it. */ public List readAllFromStorage(StorageType type) { + Preconditions.checkNotNull(type); Input input = Input.newBuilder() .setType(CommandType.READ_ALL_FROM_STORAGE) .setStorageEntry(GenericStorageEntry.newBuilder() @@ -965,13 +1031,15 @@ public List readAllFromStorage(StorageType type) { public void reload() { // Ignore output. evaluateAsynchronously( - Input.newBuilder().setType(Input.CommandType.RELOAD), null, null); + Input.newBuilder().setType(Input.CommandType.RELOAD), Optional.absent(), + Optional.absent()); } /** * Sends SEND_USER_DICTIONARY_COMMAND to edit user dictionaries. */ public UserDictionaryCommandStatus sendUserDictionaryCommand(UserDictionaryCommand command) { + Preconditions.checkNotNull(command); Output output = evaluateSynchronously(Input.newBuilder() .setType(CommandType.SEND_USER_DICTIONARY_COMMAND) .setUserDictionaryCommand(command) @@ -982,26 +1050,34 @@ public UserDictionaryCommandStatus sendUserDictionaryCommand(UserDictionaryComma /** * Sends an UPDATE_REQUEST command to the evaluation thread. */ - public void updateRequest(Request update, List touchEventList) { + public void updateRequest(Request update, List touchEventList) { + Preconditions.checkNotNull(update); + Preconditions.checkNotNull(touchEventList); + Preconditions.checkState(handler.isPresent()); Input.Builder inputBuilder = Input.newBuilder() .setRequest(update) .addAllTouchEvents(touchEventList); - handler.sendMessage(handler.obtainMessage(ExecutorMainCallback.UPDATE_REQUEST, inputBuilder)); + handler.get().sendMessage( + handler.get().obtainMessage(ExecutorMainCallback.UPDATE_REQUEST, inputBuilder)); } public void sendKeyEvent(KeyEventInterface triggeringKeyEvent, EvaluationCallback callback) { - KeyEventCallBackContext context = new KeyEventCallBackContext( - triggeringKeyEvent, callback, callbackHandler); - handler.sendMessage(handler.obtainMessage(ExecutorMainCallback.PASS_TO_CALLBACK, context)); + Preconditions.checkNotNull(triggeringKeyEvent); + Preconditions.checkNotNull(callback); + Preconditions.checkState(handler.isPresent()); + KeyEventCallbackContext context = + new KeyEventCallbackContext(triggeringKeyEvent, callback, callbackHandler); + handler.get().sendMessage( + handler.get().obtainMessage(ExecutorMainCallback.PASS_TO_CALLBACK, context)); } public void sendUsageStatsEvent(UsageStatsEvent event) { evaluateAsynchronously( Input.newBuilder() - .setType(CommandType.SEND_COMMAND) - .setCommand(SessionCommand.newBuilder() - .setType(SessionCommand.CommandType.USAGE_STATS_EVENT) - .setUsageStatsEvent(event)), - null, null); + .setType(CommandType.SEND_COMMAND) + .setCommand(SessionCommand.newBuilder() + .setType(SessionCommand.CommandType.USAGE_STATS_EVENT) + .setUsageStatsEvent(event)), + Optional.absent(), Optional.absent()); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/session/SessionHandlerFactory.java b/src/android/src/com/google/android/inputmethod/japanese/session/SessionHandlerFactory.java index 691163284..e35275207 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/session/SessionHandlerFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/session/SessionHandlerFactory.java @@ -31,6 +31,8 @@ import org.mozc.android.inputmethod.japanese.MozcLog; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import android.content.Context; import android.content.SharedPreferences; @@ -48,6 +50,7 @@ * */ public class SessionHandlerFactory { + @VisibleForTesting static final String PREF_TWEAK_USE_SOCKET_SESSION_HANDLER_KEY = "pref_tweak_use_socket_session_handler"; @VisibleForTesting static final String PREF_TWEAK_SOCKET_SESSION_HANDLER_ADDRESS_KEY = @@ -55,25 +58,26 @@ public class SessionHandlerFactory { @VisibleForTesting static final String PREF_TWEAK_SOCKET_SESSION_HANDLER_PORT_KEY = "pref_tweak_socket_session_handler_port"; - private final SharedPreferences sharedPreferences; + private final Optional sharedPreferences; public SessionHandlerFactory(Context context) { - this(context == null ? null : PreferenceManager.getDefaultSharedPreferences(context)); + this(Optional.of( + PreferenceManager.getDefaultSharedPreferences(Preconditions.checkNotNull(context)))); } /** * @param sharedPreferences the preferences. The type to be created is based on the preference. */ - public SessionHandlerFactory(SharedPreferences sharedPreferences) { - this.sharedPreferences = sharedPreferences; + public SessionHandlerFactory(Optional sharedPreferences) { + this.sharedPreferences = Preconditions.checkNotNull(sharedPreferences); } /** * Creates a session handler. */ public SessionHandler create() { - if (sharedPreferences != null && - sharedPreferences.getBoolean(PREF_TWEAK_USE_SOCKET_SESSION_HANDLER_KEY, false)) { + if (sharedPreferences.isPresent() + && sharedPreferences.get().getBoolean(PREF_TWEAK_USE_SOCKET_SESSION_HANDLER_KEY, false)) { try { MozcLog.i("Trying to connect to Mozc server via network"); // If PREF_TWEAK_USE_SOCKET_SESSION_HANDLER_KEY is enabled, @@ -81,10 +85,10 @@ public SessionHandler create() { // "10.0.2.2" is the host PC's address in emulator environment. // 8000 is the server's default port. int port = - Integer.parseInt(sharedPreferences.getString( + Integer.parseInt(sharedPreferences.get().getString( PREF_TWEAK_SOCKET_SESSION_HANDLER_PORT_KEY, "8000")); InetAddress hostAddress = - InetAddress.getByName(sharedPreferences.getString( + InetAddress.getByName(sharedPreferences.get().getString( PREF_TWEAK_SOCKET_SESSION_HANDLER_ADDRESS_KEY, "10.0.2.2")); SocketSessionHandler socketSessionHandler = new SocketSessionHandler(hostAddress, port); if (socketSessionHandler.isReachable()) { diff --git a/src/android/src/com/google/android/inputmethod/japanese/session/SocketSessionHandler.java b/src/android/src/com/google/android/inputmethod/japanese/session/SocketSessionHandler.java index 2546393de..6d2e5dcdc 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/session/SocketSessionHandler.java +++ b/src/android/src/com/google/android/inputmethod/japanese/session/SocketSessionHandler.java @@ -32,6 +32,7 @@ import org.mozc.android.inputmethod.japanese.MozcUtil; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output; +import com.google.common.base.Preconditions; import android.content.Context; @@ -58,7 +59,7 @@ class SocketSessionHandler implements SessionHandler { private final InetAddress host; private final int port; SocketSessionHandler(InetAddress host, int port) { - this.host = host; + this.host = Preconditions.checkNotNull(host); this.port = port; } @@ -69,6 +70,8 @@ public void initialize(Context context) { @Override public Command evalCommand(Command command) { + Preconditions.checkNotNull(command); + // We should be in a worker thread so below invocation is not needed. Just in case. MozcUtil.relaxMainthreadStrictMode(); try { diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/CandidateLayoutRenderer.java b/src/android/src/com/google/android/inputmethod/japanese/ui/CandidateLayoutRenderer.java index 18f3860ba..a29ee1e60 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ui/CandidateLayoutRenderer.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/CandidateLayoutRenderer.java @@ -29,12 +29,12 @@ package org.mozc.android.inputmethod.japanese.ui; -import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList; import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Row; import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span; import org.mozc.android.inputmethod.japanese.view.CarrierEmojiRenderHelper; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; @@ -42,17 +42,18 @@ import android.annotation.SuppressLint; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build; import android.text.Layout; import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; -import android.util.FloatMath; -import android.view.View; import java.util.List; +import java.util.Locale; /** * Renders the {@link CandidateLayout} instance to the {@link Canvas}. @@ -102,6 +103,11 @@ public enum DescriptionLayoutPolicy { * won't be overlapped). */ EXCLUSIVE, + + /** + * Like View.GONE, the descriptor is not shown and doesn't occupy any are. + */ + GONE, } private static final int[] STATE_EMPTY = {}; @@ -110,9 +116,14 @@ public enum DescriptionLayoutPolicy { // Should not edit its contents. private static final int[] STATE_FOCUSED = { android.R.attr.state_focused }; - private final CarrierEmojiRenderHelper carrierEmojiRenderHelper; + /** Locale field for {@link Paint#setTextLocale(Locale)}. */ + private static final Optional TEXT_LOCALE = (Build.VERSION.SDK_INT >= 17) + ? Optional.of(Locale.JAPAN) : Optional.absent(); + private final TextPaint valuePaint = createTextPaint(true, Color.BLACK, Align.LEFT); + private final TextPaint focusedValuePaint = createTextPaint(true, Color.BLACK, Align.LEFT); private final TextPaint descriptionPaint = createTextPaint(true, Color.GRAY, Align.RIGHT); + private final Paint separatorPaint = new Paint(); /** * The cache of Rect instance for the clip used in drawCandidateList method to reduce the @@ -130,18 +141,21 @@ public enum DescriptionLayoutPolicy { private ValueScalingPolicy valueScalingPolicy = ValueScalingPolicy.UNIFORM; private DescriptionLayoutPolicy descriptionLayoutPolicy = DescriptionLayoutPolicy.OVERLAY; - private Optional spanBackgroundDrawable; + private Optional spanBackgroundDrawable = Optional.absent(); @VisibleForTesting int focusedIndex = -1; - public CandidateLayoutRenderer(View targetView) { - carrierEmojiRenderHelper = new CarrierEmojiRenderHelper(Preconditions.checkNotNull(targetView)); + public CandidateLayoutRenderer() { } + @SuppressLint("NewApi") private static TextPaint createTextPaint(boolean antiAlias, int color, Align align) { TextPaint textPaint = new TextPaint(); textPaint.setAntiAlias(antiAlias); textPaint.setColor(color); textPaint.setTextAlign(Preconditions.checkNotNull(align)); + if (TEXT_LOCALE.isPresent()) { + textPaint.setTextLocale(TEXT_LOCALE.get()); + } return textPaint; } @@ -151,10 +165,16 @@ private static boolean isFocused( return (index == focusedIndex) || (index == pressedCandidateIndex); } + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + valuePaint.setColor(skin.candidateValueTextColor); + focusedValuePaint.setColor(skin.candidateValueFocusedTextColor); + descriptionPaint.setColor(skin.candidateDescriptionTextColor); + } + public void setValueTextSize(float valueTextSize) { this.valueTextSize = valueTextSize; this.valuePaint.setTextSize(valueTextSize); - this.carrierEmojiRenderHelper.setCandidateTextSize(valueTextSize); } public void setValueHorizontalPadding(float valueHorizontalPadding) { @@ -182,11 +202,6 @@ public void setDescriptionLayoutPolicy(DescriptionLayoutPolicy descriptionLayout this.descriptionLayoutPolicy = Preconditions.checkNotNull(descriptionLayoutPolicy); } - public void setEmojiProviderType(EmojiProviderType emojiProviderType) { - this.carrierEmojiRenderHelper.setEmojiProviderType( - Preconditions.checkNotNull(emojiProviderType)); - } - public void setSpanBackgroundDrawable(Optional drawable) { this.spanBackgroundDrawable = Preconditions.checkNotNull(drawable); } @@ -196,31 +211,22 @@ public void setCandidateList(Optional candidateList) { focusedIndex = (candidateList.isPresent() && candidateList.get().hasFocusedIndex()) ? candidateList.get().getFocusedIndex() : -1; - carrierEmojiRenderHelper.setCandidateList(candidateList); } - /** - * In order to support emoji rendering, the client of this class must invoke this method - * in its onAttachedToWindow method. - */ - public void onAttachedToWindow() { - carrierEmojiRenderHelper.onAttachedToWindow(); + public void setSeparatorWidth(float separatorWidth) { + separatorPaint.setStrokeWidth(separatorWidth); } - /** - * In order to support emoji rendering, the client of this class must invoke this method - * in its onDeachedFromWindow method. - */ - @SuppressLint("MissingSuperCall") - public void onDetachedFromWindow() { - carrierEmojiRenderHelper.onDetachedFromWindow(); + public void setSeparatorColor(int color) { + separatorPaint.setColor(color); } /** * Renders the {@code candidateLayout} to the given {@code canvas}. */ public void drawCandidateLayout( - Canvas canvas, CandidateLayout candidateLayout, int pressedCandidateIndex) { + Canvas canvas, CandidateLayout candidateLayout, int pressedCandidateIndex, + CarrierEmojiRenderHelper carrierEmojiRenderHelper) { Preconditions.checkNotNull(canvas); Preconditions.checkNotNull(candidateLayout); @@ -237,6 +243,10 @@ public void drawCandidateLayout( continue; } + float separatorMargin = row.getHeight() * 0.2f; + float separatorTop = row.getTop() + separatorMargin; + float separatorBottom = row.getTop() + row.getHeight() - separatorMargin; + for (Span span : row.getSpanList()) { if (span.getLeft() > clipBounds.right) { break; @@ -244,16 +254,23 @@ public void drawCandidateLayout( if (span.getRight() < clipBounds.left) { continue; } - - if (span.getCandidateWord().isPresent()) { - drawSpan(canvas, row, span, - isFocused(span.getCandidateWord().get(), focusedIndex, pressedCandidateIndex)); + // Even if span.getCandidateWord() is absent, draw the span in order to draw the background. + drawSpan(canvas, row, span, + span.getCandidateWord().isPresent() + && isFocused(span.getCandidateWord().get(), + focusedIndex, pressedCandidateIndex), + carrierEmojiRenderHelper); + if (span.getLeft() != 0f) { + float separatorX = span.getLeft(); + canvas.drawLine(separatorX, separatorTop, separatorX, separatorBottom, separatorPaint); } } } } - @VisibleForTesting void drawSpan(Canvas canvas, Row row, Span span, boolean isFocused) { + @VisibleForTesting void drawSpan( + Canvas canvas, Row row, Span span, boolean isFocused, + CarrierEmojiRenderHelper carrierEmojiRenderHelper) { drawSpanBackground( Preconditions.checkNotNull(canvas), Preconditions.checkNotNull(row), span, isFocused); if (!span.getCandidateWord().isPresent()) { @@ -261,15 +278,9 @@ public void drawCandidateLayout( } if (carrierEmojiRenderHelper.isRenderableEmoji(span.getCandidateWord().get().getValue())) { - drawCarrierEmoji(canvas, row, span); - if (descriptionLayoutPolicy == DescriptionLayoutPolicy.OVERLAY) { - // Hack: This is a quick hack to keep the current behavior (especially on SymbolInputView). - // TODO(hidehiko): Remove this hack from here, instead, do not attach the description - // to the candidate. - return; - } + drawCarrierEmoji(canvas, row, span, carrierEmojiRenderHelper); } else { - drawText(canvas, row, span); + drawText(canvas, row, span, isFocused); } drawDescription(canvas, row, span); @@ -289,7 +300,8 @@ private void drawSpanBackground(Canvas canvas, Row row, Span span, boolean isFoc spanBackgroundDrawable.draw(canvas); } - private void drawCarrierEmoji(Canvas canvas, Row row, Span span) { + private void drawCarrierEmoji( + Canvas canvas, Row row, Span span, CarrierEmojiRenderHelper carrierEmojiRenderHelper) { Preconditions.checkState(span.getCandidateWord().isPresent()); float descriptionWidth = (descriptionLayoutPolicy == DescriptionLayoutPolicy.EXCLUSIVE) @@ -300,7 +312,7 @@ private void drawCarrierEmoji(Canvas canvas, Row row, Span span) { carrierEmojiRenderHelper.drawEmoji(canvas, span.getCandidateWord().get(), centerX, centerY); } - private void drawText(Canvas canvas, Row row, Span span) { + private void drawText(Canvas canvas, Row row, Span span, boolean isFocused) { Preconditions.checkState(span.getCandidateWord().isPresent()); String valueText = span.getCandidateWord().get().getValue(); @@ -308,25 +320,26 @@ private void drawText(Canvas canvas, Row row, Span span) { // No value is available. return; } - - if (!span.getCachedLayout().isPresent()) { + // Calculate layout or get cached one. + // If isFocused is true, special paint should be applied. + // The resulting drawing is so special that it will not re reused. + // Therefore if isFocused is true cache is not used and always calculate the layout. + // In this case calculated layout is not cached. + Layout layout; + if (!isFocused && span.getCachedLayout().isPresent()) { + layout = span.getCachedLayout().get(); + } else { // Set the scaling of the text. float descriptionWidth = (descriptionLayoutPolicy == DescriptionLayoutPolicy.EXCLUSIVE) ? span.getDescriptionWidth() : 0; + // Ensure that StaticLayout instance has positive width. float displayValueWidth = - span.getWidth() - valueHorizontalPadding * 2 - descriptionWidth; + Math.max(1f, span.getWidth() - valueHorizontalPadding * 2 - descriptionWidth); float textScale = Math.min(1f, displayValueWidth / span.getValueWidth()); - TextPaint valuePaint = this.valuePaint; + TextPaint textPaint = isFocused ? this.focusedValuePaint : this.valuePaint; if (valueScalingPolicy == ValueScalingPolicy.HORIZONTAL) { - valuePaint.setTextSize(valueTextSize); - valuePaint.setTextScaleX(textScale); - // The scaled rendered text sometimes exceeds the available width for some reasons. - // In this case, the candidate word is unexpectedly rendered in two lines. - // To avoid this situation, textScale is gradually reduced to fit the available width. - for (int i = 0; valuePaint.measureText(valueText) > displayValueWidth && i < 10; ++i) { - textScale *= 0.97; - valuePaint.setTextScaleX(textScale); - } + textPaint.setTextSize(valueTextSize); + textPaint.setTextScaleX(textScale); } else { // Calculate the max limit of the "text size", in which we can render the candidate text // inside the given span. @@ -334,19 +347,25 @@ private void drawText(Canvas canvas, Row row, Span span) { // Adjustment by font size can keep aspect ratio, // which is important for Emoticon especially. // Calculate the width with the default text size. - valuePaint.setTextSize(valueTextSize * textScale); + textPaint.setTextSize(valueTextSize * textScale); + } + // Layout's width is theoretically `span.getWidth() - descriptionWidth`. + // However because of the spec of Paint#setTextScaleX() and Paint#setTextSize(), + // Paint#measureText() might return larger width than what both above methods expect it to be. + // As a workaround, if theoretical width is smaller than the result of Paint#measureText(), + // employ the width returned by Paint#measureText(). + // This workaround is to avoid from unexpected line-break. + // NOTE: Canvas#scale() cannot be used here because we have to use StaticLayout to draw + // Emoji and StaticLayout requires width in its constructor. + layout = new StaticLayout( + valueText, new TextPaint(textPaint), + (int) Math.ceil(Math.max(span.getWidth() - descriptionWidth, + textPaint.measureText(valueText))), + Alignment.ALIGN_CENTER, 1, 0, false); + if (!isFocused) { + span.setCachedLayout(layout); } - - // Layout the text. In order to avoid unexpected line breaking, we include the horizontal - // padding (on both sides) into the width for the layout. It should be safe, - // because the Alignment is ALIGN_CENTER, so that having the padding should have - // no bad effect. - span.setCachedLayout(new StaticLayout( - valueText, new TextPaint(valuePaint), - (int) FloatMath.ceil(span.getWidth() - descriptionWidth), - Alignment.ALIGN_CENTER, 1, 0, false)); } - Layout layout = span.getCachedLayout().get(); // Actually render the image to the canvas. int saveCount = canvas.save(); @@ -360,19 +379,25 @@ valueText, new TextPaint(valuePaint), private void drawDescription(Canvas canvas, Row row, Span span) { List descriptionList = span.getSplitDescriptionList(); - if (span.getDescriptionWidth() <= 0 || descriptionList.isEmpty()) { - // No description available. + if (span.getDescriptionWidth() <= 0 || descriptionList.isEmpty() + || descriptionLayoutPolicy == DescriptionLayoutPolicy.GONE) { + // No description available or the layout policy is GONE. return; } // Set the x-orientation scale based on the description's width to fit the span's region. TextPaint descriptionPaint = this.descriptionPaint; descriptionPaint.setTextSize(descriptionTextSize); + float centerOrRight; if (descriptionLayoutPolicy == DescriptionLayoutPolicy.OVERLAY) { float displayWidth = span.getWidth() - descriptionHorizontalPadding * 2; descriptionPaint.setTextScaleX(Math.min(1f, displayWidth / span.getDescriptionWidth())); + descriptionPaint.setTextAlign(Align.CENTER); + centerOrRight = (span.getLeft() + span.getRight()) / 2f; } else { descriptionPaint.setTextScaleX(1f); + descriptionPaint.setTextAlign(Align.RIGHT); + centerOrRight = span.getRight() - descriptionHorizontalPadding; } // Render first "N" description lines based on the layout height. @@ -383,9 +408,8 @@ private void drawDescription(Canvas canvas, Row row, Span span) { float top = row.getTop() + row.getHeight() - descriptionVerticalPadding - descriptionTextSize * (numDescriptionLines - 1); - float right = span.getRight() - descriptionHorizontalPadding; for (String description : descriptionList.subList(0, numDescriptionLines)) { - canvas.drawText(description, right, top, descriptionPaint); + canvas.drawText(description, centerOrRight, top, descriptionPaint); top += descriptionTextSize; } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/ConversionCandidateLayouter.java b/src/android/src/com/google/android/inputmethod/japanese/ui/ConversionCandidateLayouter.java index 6c527c7c1..658082055 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ui/ConversionCandidateLayouter.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/ConversionCandidateLayouter.java @@ -33,11 +33,10 @@ import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Row; import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; -import android.util.FloatMath; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -84,7 +83,7 @@ int getNumChunks(Span span) { Preconditions.checkNotNull(span); float compressedValueWidth = compressValueWidth(span.getValueWidth(), compressionRatio, horizontalPadding, minWidth); - return (int) FloatMath.ceil((compressedValueWidth + span.getDescriptionWidth()) / chunkWidth); + return (int) Math.ceil((compressedValueWidth + span.getDescriptionWidth()) / chunkWidth); } static float compressValueWidth( @@ -196,7 +195,7 @@ public int getPageWidth() { } public int getRowHeight() { - return (int) FloatMath.ceil(valueHeight + valueVerticalPadding * 2); + return (int) Math.ceil(valueHeight + valueVerticalPadding * 2); } @Override @@ -252,6 +251,7 @@ public Optional layout(CandidateList candidateList) { * * The order of the candidates will be kept. */ + @VisibleForTesting static List buildRowList( CandidateList candidateList, SpanFactory spanFactory, int numChunks, ChunkMetrics chunkMetrics, boolean enableSpan) { @@ -297,6 +297,7 @@ static List buildRowList( * The size of the buffer must be equal to or greater than {@code spanList.size()}. * Its elements needn't be initialized. */ + @VisibleForTesting static void layoutSpanList( List spanList, int pageWidth, int numChunks, ChunkMetrics chunkMetrics, int[] numAllocatedChunks) { @@ -317,10 +318,9 @@ static void layoutSpanList( } } - // Then assign remaining chunks to each span as even as possible by round-robin - // from tail to head to keep the backward compatibility. - for (int index = spanList.size() - 1; numRemainingChunks > 0; - --numRemainingChunks, index = (index + spanList.size() - 1) % spanList.size()) { + // Then assign remaining chunks to each span as even as possible by round-robin. + for (int index = 0; numRemainingChunks > 0; + --numRemainingChunks, index = (index + 1) % spanList.size()) { ++numAllocatedChunks[index]; } @@ -344,6 +344,7 @@ static void layoutSpanList( } /** Sets top, width and height to the each row. */ + @VisibleForTesting static void layoutRowList(List rowList, int pageWidth, int rowHeight) { int top = 0; for (Row row : Preconditions.checkNotNull(rowList)) { diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/FloatingCandidateLayoutRenderer.java b/src/android/src/com/google/android/inputmethod/japanese/ui/FloatingCandidateLayoutRenderer.java new file mode 100644 index 000000000..1376ce238 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/FloatingCandidateLayoutRenderer.java @@ -0,0 +1,500 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.ui; + +import org.mozc.android.inputmethod.japanese.MozcUtil; +import org.mozc.android.inputmethod.japanese.ViewEventListener; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.Candidates; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.Candidates.Candidate; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.Category; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; +import org.mozc.android.inputmethod.japanese.resources.R; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.FontMetrics; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.view.MotionEvent; + +import java.util.Locale; + +/** + * Layouts floating candidate window and draw it's contents on canvas. + * + * The point of origin of layout is NOT a left-top corner of candidate list BUT the left-top corner + * of the candidate column and the right-top corner of the shortcut column. + * + * TODO(hsumita): Rewrite using LinearLayout or something. + */ +public class FloatingCandidateLayoutRenderer { + + private static class WindowRects { + + public final Rect window; + public final Optional focus; + public final Optional pageIndicator; + public final Optional scrollIndicator; + + WindowRects(Rect window, Optional focus, Optional pageIndicator, + Optional scrollIndicator) { + this.window = Preconditions.checkNotNull(window); + this.focus = Preconditions.checkNotNull(focus); + this.pageIndicator = Preconditions.checkNotNull(pageIndicator); + this.scrollIndicator = Preconditions.checkNotNull(scrollIndicator); + } + } + + /** Locale field for {@link Paint#setTextLocale(Locale)}. */ + private static final Optional TEXT_LOCALE = (Build.VERSION.SDK_INT >= 17) + ? Optional.of(Locale.JAPAN) : Optional.absent(); + + private static final String FOOTER_TEXT_FORMAT = "%d / %d"; + + private final Paint candidatePaint; + private final Paint focusedCandidatePaint; + private final Paint descriptionPaint; + private final Paint shortcutPaint; + private final Paint footerPaint; + private final Paint separatorPaint; + private final Paint windowBackgroundPaint; + private final Paint focuseBackgroundPaint; + private final Paint scrollIndicatorPaint; + + private final int windowMinimumWidth; + private final int windowHorizontalPadding; + private final float windowRoundRectRadius; + private final int candidateHeight; + private final int candidateOffsetY; + private final int candidateDescriptionMinimumPadding; + private final int footerHeight; + private final float footerTextCenterToBaseLineOffset; + private final int horizontalSeparatorPadding; + private final int shortcutWidth; + private final float shortcutCenterX; + private final int scrollIndicatorWidth; + private final int scrollIndicatorRadius; + + private Optional windowRects = Optional.absent(); + private Optional viewEventListener = Optional.absent(); + private Optional candidates = Optional.absent(); + private Optional maxWidth = Optional.absent(); + /** Focused candidate index, or tapped candidate index if exists. */ + private Optional focusedOrTappedCandidateIndexOnPage = Optional.absent(); + /** TappedInfo for the current touch operation. Set on TOUCH_DOWN, reset on TOUCH_UP. */ + private Optional tappingCandidateIndex = Optional.absent(); + private int totalCandidatesCount; + private int maxCandidateWidth; + private int maxDescriptionWidth; + + public FloatingCandidateLayoutRenderer(Resources res) { + Preconditions.checkNotNull(res); + + candidatePaint = new Paint(); + candidatePaint.setColor(res.getColor(R.color.floating_candidate_text)); + candidatePaint.setTextSize(res.getDimension(R.dimen.floating_candidate_text_size)); + candidatePaint.setAntiAlias(true); + if (TEXT_LOCALE.isPresent()) { + candidatePaint.setTextLocale(TEXT_LOCALE.get()); + } + + focusedCandidatePaint = new Paint(candidatePaint); + focusedCandidatePaint.setColor(res.getColor(R.color.floating_candidate_focused_text)); + + descriptionPaint = new Paint(candidatePaint); + descriptionPaint.setTextSize( + res.getDimension(R.dimen.floating_candidate_description_text_size)); + descriptionPaint.setColor(res.getColor(R.color.floating_candidate_description_text)); + + shortcutPaint = new Paint(candidatePaint); + shortcutPaint.setTextSize(res.getDimension(R.dimen.floating_candidate_shortcut_text_size)); + shortcutPaint.setColor(res.getColor(R.color.floating_candidate_shortcut_text)); + + scrollIndicatorPaint = new Paint(); + scrollIndicatorPaint.setColor(res.getColor(R.color.floating_candidate_scroll_indicator)); + + footerPaint = new Paint(candidatePaint); + footerPaint.setTextSize(res.getDimension(R.dimen.floating_candidate_footer_text_size)); + footerPaint.setColor(res.getColor(R.color.floating_candidate_footer_text)); + + separatorPaint = new Paint(); + separatorPaint.setStrokeWidth( + res.getDimension(R.dimen.floating_candidate_separator_width)); + separatorPaint.setColor(res.getColor(R.color.floating_candidate_footer_separator)); + + windowBackgroundPaint = new Paint(); + windowBackgroundPaint.setColor(res.getColor(R.color.floating_candidate_window_background)); + windowBackgroundPaint.setShadowLayer( + res.getDimension(R.dimen.floating_candidate_window_shadow_radius), + 0, res.getDimension(R.dimen.floating_candidate_window_shadow_offset_y), + res.getColor(R.color.floating_candidate_shadow)); + + focuseBackgroundPaint = new Paint(); + focuseBackgroundPaint.setColor(res.getColor(R.color.floating_candidate_focus_background)); + + float candidateVerticalPadding = + res.getDimension(R.dimen.floating_candidate_candidate_vertical_padding); + FontMetrics candidateMetrics = candidatePaint.getFontMetrics(); + candidateHeight = (int) Math.ceil( + candidateMetrics.descent - candidateMetrics.ascent + candidateVerticalPadding * 2); + candidateOffsetY = (int) Math.ceil(-candidateMetrics.ascent + candidateVerticalPadding); + + windowMinimumWidth = res.getDimensionPixelSize(R.dimen.floating_candidate_window_minimum_width); + windowHorizontalPadding = + res.getDimensionPixelOffset(R.dimen.floating_candidate_window_horizontal_padding); + windowRoundRectRadius = res.getDimension(R.dimen.floating_candidate_window_round_rect_radius); + candidateDescriptionMinimumPadding = + res.getDimensionPixelSize(R.dimen.floating_candidate_candidate_description_minimum_padding); + horizontalSeparatorPadding = + res.getDimensionPixelSize(R.dimen.floating_candidate_separator_horizontal_padding); + + scrollIndicatorWidth = + res.getDimensionPixelSize(R.dimen.floating_candidate_scroll_indicator_width); + scrollIndicatorRadius = + res.getDimensionPixelSize(R.dimen.floating_candidate_scroll_indicator_radius); + + FontMetrics footerMetrics = footerPaint.getFontMetrics(); + float footerTextHeight = -footerMetrics.ascent + footerMetrics.descent; + footerHeight = Math.round(footerTextHeight * 2f); + footerTextCenterToBaseLineOffset = (-footerMetrics.ascent - footerMetrics.descent) / 2f; + + float shortcutCharacterWidth = shortcutPaint.measureText("m"); + float shortcutCandidatePadding = + res.getDimensionPixelSize(R.dimen.floating_candidate_shortcut_candidate_padding); + shortcutWidth = Math.round(shortcutCharacterWidth + shortcutCandidatePadding); + shortcutCenterX = -shortcutCharacterWidth / 2f - shortcutCandidatePadding; + + updateLayout(); + } + + /** Handle touch event and invoke some actions. */ + public void onTouchEvent(MotionEvent event) { + if (!candidates.isPresent() || !viewEventListener.isPresent()) { + return; + } + ViewEventListener listener = viewEventListener.get(); + + Optional optionalCandidateIndex = getTappingCandidate(event); + + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + tappingCandidateIndex = optionalCandidateIndex; + updateLayout(); + return; + } + if (event.getActionMasked() != MotionEvent.ACTION_UP) { + return; + } + + if (!optionalCandidateIndex.isPresent() || !tappingCandidateIndex.isPresent() + || !optionalCandidateIndex.equals(tappingCandidateIndex)) { + tappingCandidateIndex = Optional.absent(); + updateLayout(); + return; + } + int candidateIndex = optionalCandidateIndex.get(); + tappingCandidateIndex = Optional.absent(); + + listener.onConversionCandidateSelected( + candidates.get().getCandidate(candidateIndex).getId(), + Optional.absent()); + } + + /** Sets the max width of this window. */ + public void setMaxWidth(int maxWidth) { + if (maxWidth > 0) { + this.maxWidth = Optional.of(maxWidth); + } else { + this.maxWidth = Optional.absent(); + } + updateLayout(); + } + + /** Sets candidates. */ + public void setCandidates(Command outCommand) { + Preconditions.checkNotNull(outCommand); + if (outCommand.getOutput().getCandidates().getCandidateCount() == 0) { + candidates = Optional.absent(); + totalCandidatesCount = 0; + } else { + candidates = Optional.of(outCommand.getOutput().getCandidates()); + totalCandidatesCount = outCommand.getOutput().getAllCandidateWords().getCandidatesCount(); + } + updateLayout(); + } + + /** Sets a view event listener to handle touch events. */ + public void setViewEventListener(ViewEventListener listener) { + viewEventListener = Optional.of(listener); + } + + /** + * Gets the rectangle of this window. + * Defensive-copied value is returned so caller-side can modify it. + */ + public Optional getWindowRect() { + if (windowRects.isPresent()) { + return Optional.of(new Rect(windowRects.get().window)); + } else { + return Optional.absent(); + } + } + + /** Draws this candidate window. */ + public void draw(Canvas canvas) { + Preconditions.checkNotNull(canvas); + Preconditions.checkState(candidates.isPresent()); + Preconditions.checkState(windowRects.isPresent()); + + Candidates candidatesData = candidates.get(); + WindowRects rects = windowRects.get(); + + canvas.drawRoundRect( + new RectF(rects.window), windowRoundRectRadius, windowRoundRectRadius, + windowBackgroundPaint); + + if (rects.focus.isPresent()) { + canvas.drawRect(rects.focus.get(), focuseBackgroundPaint); + } + + // Candidates, descriptions and shortcuts. + int focusedIndex = focusedOrTappedCandidateIndexOnPage.or(-1); + for (int i = 0; i < candidatesData.getCandidateCount(); ++i) { + Candidate candidate = candidatesData.getCandidate(i); + int offsetY = getCandidateRowOffsetY(i) + candidateOffsetY; + Paint paint = (i == focusedIndex) ? focusedCandidatePaint : candidatePaint; + drawTextWithLimit(canvas, candidate.getValue(), paint, 0, offsetY, maxCandidateWidth); + if (candidate.getAnnotation().hasDescription()) { + drawTextWithAlignAndLimit( + canvas, candidate.getAnnotation().getDescription(), descriptionPaint, + rects.window.right - windowHorizontalPadding, offsetY, + Align.RIGHT, maxDescriptionWidth); + } + if (candidate.getAnnotation().hasShortcut()) { + drawTextWithAlign( + canvas, candidate.getAnnotation().getShortcut(), shortcutPaint, + shortcutCenterX, offsetY, Align.CENTER); + } + } + + // Footer. Don't show if suggestion mode. + if (rects.pageIndicator.isPresent()) { + Rect indicatorRect = rects.pageIndicator.get(); + drawHorizontalSeparator( + canvas, separatorPaint, rects.window.left, rects.window.right, indicatorRect.top); + drawPageIndicator(canvas, footerPaint, indicatorRect); + } + + // Scroll indicator + if (rects.scrollIndicator.isPresent()) { + canvas.drawRoundRect( + rects.scrollIndicator.get(), scrollIndicatorRadius, scrollIndicatorRadius, + scrollIndicatorPaint); + } + } + + private void drawPageIndicator(Canvas canvas, Paint paint, Rect rect) { + drawTextWithAlign( + canvas, String.format(FOOTER_TEXT_FORMAT, + candidates.get().getFocusedIndex() + 1, totalCandidatesCount), + paint, rect.exactCenterX(), rect.exactCenterY() + footerTextCenterToBaseLineOffset, + Align.CENTER); + } + + private void drawHorizontalSeparator(Canvas canvas, Paint paint, int startX, int endX, int y) { + canvas.drawLine( + Math.min(startX, endX) + horizontalSeparatorPadding, y, + Math.max(startX, endX) - horizontalSeparatorPadding, y, paint); + } + + /** + * Draws {@code text} into {@code canvas} with the text align and the limitation of text width. + *

+ * If measured width of {@code text} is wider than maxWidth, the {@code text} is drawn with + * horizontal compression in order to fit {@code maxWidth}. + */ + private void drawTextWithAlignAndLimit( + Canvas canvas, String text, Paint paint, float x, float y, Align align, float maxWidth) { + float textWidth = paint.measureText(text); + + int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + Align originalAlign = paint.getTextAlign(); + try { + canvas.translate(x, y); + if (textWidth > maxWidth) { + // Use Canvas#scale() instead of Paint#setTextScaleX() for accurate scaling. + canvas.scale(maxWidth / textWidth, 1.0f); + } + paint.setTextAlign(align); + canvas.drawText(text, 0, 0, paint); + } finally { + canvas.restoreToCount(saveCount); + paint.setTextAlign(originalAlign); + } + } + + /** See {@link #drawTextWithAlignAndLimit}. */ + private void drawTextWithAlign( + Canvas canvas, String text, Paint paint, float x, float y, Align align) { + drawTextWithAlignAndLimit(canvas, text, paint, x, y, align, Float.MAX_VALUE); + } + + /** See {@link #drawTextWithAlignAndLimit}. */ + private void drawTextWithLimit( + Canvas canvas, String text, Paint paint, float x, float y, float maxWidth) { + drawTextWithAlignAndLimit(canvas, text, paint, x, y, paint.getTextAlign(), maxWidth); + } + + private Optional getTappingCandidate(MotionEvent event) { + if (!windowRects.isPresent()) { + return Optional.absent(); + } + + WindowRects rects = windowRects.get(); + int x = Math.round(event.getX()); + int y = Math.round(event.getY()); + + if (!rects.window.contains(x, y)) { + return Optional.absent(); + } + + int candidateIndex = y / candidateHeight; + if (candidateIndex < candidates.get().getCandidateCount()) { + return Optional.of(candidateIndex); + } else { + return Optional.absent(); + } + } + + private void updateLayout() { + if (!candidates.isPresent() || !maxWidth.isPresent()) { + windowRects = Optional.absent(); + return; + } + + Candidates candidatesData = candidates.get(); + int candidateNumberOnPage = candidatesData.getCandidateCount(); + boolean hasShortcut = candidatesData.getCandidateCount() > 0 + && !candidatesData.getCandidate(0).getAnnotation().getShortcut().isEmpty(); + int leftEdgePosition = hasShortcut + ? -windowHorizontalPadding - shortcutWidth : -windowHorizontalPadding; + + // Candidates and descriptions + maxCandidateWidth = 0; + maxDescriptionWidth = 0; + for (int i = 0; i < candidateNumberOnPage; ++i) { + Candidate candidate = candidatesData.getCandidate(i); + maxCandidateWidth = Math.max( + maxCandidateWidth, Math.round(candidatePaint.measureText(candidate.getValue()))); + maxDescriptionWidth = Math.max( + maxDescriptionWidth, + Math.round(descriptionPaint.measureText(candidate.getAnnotation().getDescription()))); + } + int fixedWidth = + -leftEdgePosition + candidateDescriptionMinimumPadding + windowHorizontalPadding; + int flexibleWidth = maxCandidateWidth + maxDescriptionWidth; + if (fixedWidth + flexibleWidth > maxWidth.get()) { + int availableWidth = maxWidth.get() - fixedWidth; + float shrinkRate = MozcUtil.clamp((float) availableWidth / flexibleWidth, 0f, 1f); + maxDescriptionWidth = Math.round(maxDescriptionWidth * shrinkRate); + maxCandidateWidth = availableWidth - maxDescriptionWidth; + } + int rightEdgePosition = Math.max( + Math.min(windowMinimumWidth, maxWidth.get()) + leftEdgePosition, + maxCandidateWidth + candidateDescriptionMinimumPadding + maxDescriptionWidth + + windowHorizontalPadding); + + // Footer + int horizontalSeparatorY = candidateHeight * candidateNumberOnPage; + int bottomEdgePosition; + Optional pageIndicatorRect; + if (candidatesData.getCategory() != Category.SUGGESTION) { + bottomEdgePosition = horizontalSeparatorY + footerHeight; + pageIndicatorRect = Optional.of( + new Rect(leftEdgePosition, horizontalSeparatorY, rightEdgePosition, bottomEdgePosition)); + } else { + bottomEdgePosition = horizontalSeparatorY; + pageIndicatorRect = Optional.absent(); + } + + // Focus + Optional focusRect = Optional.absent(); + focusedOrTappedCandidateIndexOnPage = getTappedOrFocusedIndexOnPage(); + if (focusedOrTappedCandidateIndexOnPage.isPresent()) { + int offsetY = candidateHeight * focusedOrTappedCandidateIndexOnPage.get(); + focusRect = Optional.of(new Rect( + leftEdgePosition, offsetY, rightEdgePosition, offsetY + candidateHeight)); + } else { + focusRect = Optional.absent(); + } + + // Scroll indicator + Optional scrollIndicatorRect; + if (totalCandidatesCount > candidatesData.getPageSize()) { + int currentPageIndex = getCurrentPageNumber() - 1; + float scrollIndicatorHeight = + (float) bottomEdgePosition * candidatesData.getPageSize() / totalCandidatesCount; + float scrollIndicatorOffset = scrollIndicatorHeight * currentPageIndex; + scrollIndicatorRect = Optional.of(new RectF( + rightEdgePosition - scrollIndicatorWidth, scrollIndicatorOffset, rightEdgePosition, + Math.min(bottomEdgePosition, scrollIndicatorOffset + scrollIndicatorHeight))); + } else { + scrollIndicatorRect = Optional.absent(); + } + + // Window + Rect windowRect = new Rect(leftEdgePosition, 0, rightEdgePosition, bottomEdgePosition); + + windowRects = Optional.of( + new WindowRects(windowRect, focusRect, pageIndicatorRect, scrollIndicatorRect)); + } + + private Optional getTappedOrFocusedIndexOnPage() { + if (tappingCandidateIndex.isPresent()) { + return tappingCandidateIndex; + } else if (candidates.isPresent() && candidates.get().hasFocusedIndex()) { + return Optional.of(candidates.get().getFocusedIndex() % candidates.get().getPageSize()); + } + return Optional.absent(); + } + + private int getCandidateRowOffsetY(int index) { + return index * candidateHeight; + } + + private int getCurrentPageNumber() { + return (int) Math.ceil( + (float) (candidates.get().getFocusedIndex() + 1) / candidates.get().getPageSize()); + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/FloatingModeIndicator.java b/src/android/src/com/google/android/inputmethod/japanese/ui/FloatingModeIndicator.java new file mode 100644 index 000000000..70037e5fe --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/FloatingModeIndicator.java @@ -0,0 +1,247 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.ui; + +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input; +import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand; +import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.view.MozcImageView; +import org.mozc.android.inputmethod.japanese.view.Skin; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.AnimationSet; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.ScaleAnimation; +import android.view.inputmethod.CursorAnchorInfo; + +/** + * Draws mode indicator for floating candidate window. + */ +@TargetApi(21) +public class FloatingModeIndicator { + + /** The message to hide the mode indicator. */ + @VisibleForTesting static final int HIDE_MODE_INDICATOR = 0; + + private class OutAnimationListener implements AnimationListener { + @Override + public void onAnimationEnd(Animation animation) { + if (!isVisible) { + popup.getContentView().setVisibility(View.GONE); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + } + + private class ModeIndicatorMessageCallback implements Handler.Callback { + @Override + public boolean handleMessage(Message msg) { + if (msg.what == HIDE_MODE_INDICATOR) { + hide(); + } + return true; + } + } + + @VisibleForTesting final Handler handler; + @VisibleForTesting final PopUpLayouter popup; + private final View parentView; + private final Drawable kanaIndicatorDrawable; + private final Drawable abcIndicatorDrawable; + + private final int indicatorSize; + private final int verticalMargin; + private final Rect drawRect; + private final Animation inAnimation; + private final Animation outAnimation; + private final int displayTime; + + private CursorAnchorInfo cursorAnchorInfo = new CursorAnchorInfo.Builder().build(); + /** True if the mode indicator is shown and is not hiding. */ + private boolean isVisible = false; + private boolean hasComposition = false; + + public FloatingModeIndicator(View parent) { + parentView = Preconditions.checkNotNull(parent); + handler = new Handler(Looper.getMainLooper(), new ModeIndicatorMessageCallback()); + + Context context = parent.getContext(); + Resources resources = context.getResources(); + Skin skin = Skin.getFallbackInstance(); + kanaIndicatorDrawable = + skin.getDrawable(resources, R.raw.floating_mode_indicator__kana_normal); + abcIndicatorDrawable = + skin.getDrawable(resources, R.raw.floating_mode_indicator__alphabet_normal); + indicatorSize = + resources.getDimensionPixelSize(R.dimen.floating_mode_indicator_size); + verticalMargin = + resources.getDimensionPixelSize(R.dimen.floating_mode_indicator_vertical_margin); + displayTime = resources.getInteger(R.integer.floating_mode_indicator_display_time); + + MozcImageView contentView = new MozcImageView(context); + contentView.setVisibility(View.GONE); + contentView.setImageDrawable(kanaIndicatorDrawable); + popup = new PopUpLayouter(parentView, contentView); + + inAnimation = createInAnimation(resources, indicatorSize / 2f, verticalMargin); + outAnimation = createOutAnimation(resources, indicatorSize / 2f, verticalMargin); + drawRect = new Rect(0, 0, indicatorSize, indicatorSize); + } + + public void setCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + this.cursorAnchorInfo = Preconditions.checkNotNull(cursorAnchorInfo); + } + + private void updateDrawRect() { + float[] cursorPosition = new float[] { + cursorAnchorInfo.getInsertionMarkerHorizontal(), + cursorAnchorInfo.getInsertionMarkerBottom() + }; + cursorAnchorInfo.getMatrix().mapPoints(cursorPosition); + int location[] = new int[2]; + parentView.getLocationOnScreen(location); + int left = Math.round(cursorPosition[0] - indicatorSize / 2) - location[0]; + int top = Math.round(cursorPosition[1] + verticalMargin) - location[1]; + // TODO(hsumita): Put the indicator over the cursor if there is no enough space below. + // Note: We always have enough space below thanks to the narrow frame at this time. + drawRect.offsetTo(left, top); + popup.setBounds(drawRect); + } + + /** + * Updates the state of the mode indicator according to the {@code command}. + *

+ * This method hides the indicator if there is a composition text. + */ + public void setCommand(Command command) { + Preconditions.checkNotNull(command); + Input input = command.getInput(); + if (input.getType() == Input.CommandType.SEND_COMMAND + && input.getCommand().getType() == SessionCommand.CommandType.SWITCH_INPUT_MODE) { + // Simply ignores SWITCH_INPUT_MODE since the command doesn't have a composition text. + return; + } + + hasComposition = command.getOutput().getPreedit().getSegmentCount() > 0; + if (hasComposition) { + hide(); + } + } + + /** Shows the mode indicator according to the current composition mode. */ + public void setCompositionMode(CompositionMode mode) { + Preconditions.checkNotNull(mode); + MozcImageView contentView = popup.getContentView(); + contentView.setImageDrawable(mode == CompositionMode.HIRAGANA + ? kanaIndicatorDrawable : abcIndicatorDrawable); + if (!hasComposition) { + show(); + } + } + + /** + * Shows the mode indicator with animation. + *

+ * This method issues hide command with delay. + */ + private void show() { + resetAnimation(); + updateDrawRect(); + if (!isVisible) { + isVisible = true; + View contentView = popup.getContentView(); + contentView.setVisibility(View.VISIBLE); + contentView.startAnimation(inAnimation); + } + handler.sendMessageDelayed(handler.obtainMessage(HIDE_MODE_INDICATOR), displayTime); + } + + /** Hides the mode indicator with animation. */ + public void hide() { + if (!isVisible) { + return; + } + isVisible = false; + resetAnimation(); + popup.getContentView().startAnimation(outAnimation); + } + + private Animation createInAnimation(Resources resources, float pivotX, float pivotY) { + AnimationSet animationSet = new AnimationSet(true); + animationSet.setDuration(resources.getInteger(R.integer.floating_mode_indicator_in_duration)); + animationSet.setInterpolator(new DecelerateInterpolator()); + animationSet.addAnimation(new ScaleAnimation(0f, 1f, 0f, 1f, pivotX, pivotY)); + animationSet.addAnimation(new AlphaAnimation(0f, 1f)); + return animationSet; + } + + private Animation createOutAnimation(Resources resources, float pivotX, float pivotY) { + AnimationSet animationSet = new AnimationSet(true); + animationSet.setDuration(resources.getInteger(R.integer.floating_mode_indicator_out_duration)); + animationSet.setInterpolator(new DecelerateInterpolator()); + animationSet.setAnimationListener(new OutAnimationListener()); + animationSet.addAnimation(new ScaleAnimation(1f, 0f, 1f, 0f, pivotX, pivotY)); + animationSet.addAnimation(new AlphaAnimation(1f, 0f)); + return animationSet; + } + + /** Resets all ongoing and scheduled animations. */ + private void resetAnimation() { + popup.getContentView().clearAnimation(); + handler.removeMessages(HIDE_MODE_INDICATOR); + Preconditions.checkState(!handler.hasMessages(HIDE_MODE_INDICATOR)); + } + + @VisibleForTesting boolean isVisible() { + return isVisible; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/InputFrameFoldButtonView.java b/src/android/src/com/google/android/inputmethod/japanese/ui/InputFrameFoldButtonView.java new file mode 100644 index 000000000..337892f8a --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/InputFrameFoldButtonView.java @@ -0,0 +1,123 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.ui; + +import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.view.DummyDrawable; +import org.mozc.android.inputmethod.japanese.view.Skin; +import com.google.common.base.Preconditions; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ToggleButton; + +/** + * View class for the button to expand/fold conversion candidate view. + * + */ +public class InputFrameFoldButtonView extends ToggleButton { + + private static final int[] STATE_EMPTY = {}; + private static final int[] STATE_CHECKED = { android.R.attr.state_checked }; + + private Drawable arrowDrawable = DummyDrawable.getInstance(); + private Drawable backgroundDefaultDrawable = DummyDrawable.getInstance(); + private Drawable backgroundScrolledDrawable = DummyDrawable.getInstance(); + private boolean showBackgroundForScrolled = false; + + public InputFrameFoldButtonView(Context context) { + super(context); + } + + public InputFrameFoldButtonView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + public InputFrameFoldButtonView(Context context, AttributeSet attributeSet, int defStyle) { + super(context, attributeSet, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + setBackgroundColor(Color.TRANSPARENT); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + backgroundDefaultDrawable.setBounds(0, 0, width, height); + backgroundScrolledDrawable.setBounds(0, 0, width, height); + arrowDrawable.setBounds(0, 0, width, height); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + arrowDrawable.setState(isChecked() ? STATE_CHECKED : STATE_EMPTY); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + Drawable background = + showBackgroundForScrolled ? backgroundScrolledDrawable : backgroundDefaultDrawable; + background.draw(canvas); + arrowDrawable.draw(canvas); + } + + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + Resources resources = getResources(); + arrowDrawable = skin.getDrawable(resources, R.raw.keyboard__fold__tab) + .getConstantState().newDrawable(); + arrowDrawable.setBounds(0, 0, getWidth(), getHeight()); + backgroundDefaultDrawable = + skin.getDrawable(resources, R.raw.keyboard_fold_tab_background_default) + .getConstantState().newDrawable(); + backgroundDefaultDrawable.setBounds(0, 0, getWidth(), getHeight()); + backgroundScrolledDrawable = + skin.getDrawable(resources, R.raw.keyboard_fold_tab_background_scrolled) + .getConstantState().newDrawable(); + backgroundScrolledDrawable.setBounds(0, 0, getWidth(), getHeight()); + invalidate(); + } + + public void showBackgroundForScrolled(boolean showBackgroundForScrolled) { + if (this.showBackgroundForScrolled != showBackgroundForScrolled) { + this.showBackgroundForScrolled = showBackgroundForScrolled; + invalidate(); + } + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/MenuDialog.java b/src/android/src/com/google/android/inputmethod/japanese/ui/MenuDialog.java index 43f5f4605..7e5d385e2 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ui/MenuDialog.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/MenuDialog.java @@ -76,9 +76,6 @@ public static interface MenuDialogListener { /** Invoked when "Launch Preference Activity" item is selected. */ public void onLaunchPreferenceActivitySelected(Context context); - /** Invoked when "Voice input" item is selected. */ - public void onLaunchVoiceInputActivitySelected(Context context); - /** Invoked when "Launch Mushroom" item is selected. */ public void onShowMushroomSelectionDialogSelected(Context context); } @@ -130,9 +127,6 @@ public void onClick(DialogInterface dialog, int which) { case R.string.menu_item_preferences: listener.get().onLaunchPreferenceActivitySelected(context); break; - case R.string.menu_item_voice_input: - listener.get().onLaunchVoiceInputActivitySelected(context); - break; case R.string.menu_item_mushroom: listener.get().onShowMushroomSelectionDialogSelected(context); break; @@ -145,8 +139,7 @@ public void onClick(DialogInterface dialog, int which) { private final Optional dialog; private final MenuDialogListenerHandler listenerHandler; - public MenuDialog( - Context context, Optional listener, boolean isVoiceInputEnabled) { + public MenuDialog(Context context, Optional listener) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(listener); @@ -154,7 +147,7 @@ public MenuDialog( String appName = resources.getString(R.string.app_name); // R.string.menu_item_* resources needs to be formatted. - List menuItemIds = getEnabledMenuIds(context, isVoiceInputEnabled); + List menuItemIds = getEnabledMenuIds(context); int menuNum = menuItemIds.size(); String[] menuTextList = new String[menuNum]; int[] indexToIdTable = new int[menuNum]; @@ -202,7 +195,7 @@ public void setWindowToken(IBinder windowToken) { } @VisibleForTesting - static List getEnabledMenuIds(Context context, boolean isVoiceInputEnabled) { + static List getEnabledMenuIds(Context context) { // "Mushroom" item is enabled only when Mushroom-aware applications are available. PackageManager packageManager = Preconditions.checkNotNull(context).getPackageManager(); boolean isMushroomEnabled = !MushroomUtil.getMushroomApplicationList(packageManager).isEmpty(); @@ -210,9 +203,6 @@ static List getEnabledMenuIds(Context context, boolean isVoiceInputEnab List menuItemIds = Lists.newArrayListWithCapacity(4); menuItemIds.add(R.string.menu_item_input_method); menuItemIds.add(R.string.menu_item_preferences); - if (isVoiceInputEnabled) { - menuItemIds.add(R.string.menu_item_voice_input); - } if (isMushroomEnabled) { menuItemIds.add(R.string.menu_item_mushroom); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/PopUpLayouter.java b/src/android/src/com/google/android/inputmethod/japanese/ui/PopUpLayouter.java new file mode 100644 index 000000000..8a6d6d401 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/PopUpLayouter.java @@ -0,0 +1,120 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.ui; + +import org.mozc.android.inputmethod.japanese.MozcUtil; +import com.google.common.base.Preconditions; + +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.FrameLayout; + +/** + * A pop-up view layouter. + *

+ * This class registers given popup View as a direct child of root View. + * This makes the popup be able to appear to anywhere in the screen. + * To control the position a Bounds should be specified. It will be converted into a LayoutParams + * internally. + */ +public class PopUpLayouter { + + private final View parent; + private final T contentView; + + private boolean isRegistered = false; + + public PopUpLayouter(View parent, T popUpView) { + this.parent = Preconditions.checkNotNull(parent); + this.contentView = Preconditions.checkNotNull(popUpView); + } + + private void registerToViewHierarchyIfNecessary() { + if (isRegistered) { + return; + } + + View rootView = parent.getRootView(); + if (rootView != null) { + FrameLayout screenContent = + FrameLayout.class.cast(rootView.findViewById(android.R.id.content)); + if (screenContent != null) { + screenContent.addView( + contentView, new FrameLayout.LayoutParams(0, 0, Gravity.LEFT | Gravity.TOP)); + isRegistered = true; + } + } + } + + public T getContentView() { + return contentView; + } + + public void setBounds(Rect rect) { + Preconditions.checkNotNull(rect); + setBounds(rect.left, rect.top, rect.right, rect.bottom); + } + + public void setBounds(int left, int top, int right, int bottom) { + registerToViewHierarchyIfNecessary(); + + int width = right - left; + int height = bottom - top; + + ViewGroup.LayoutParams layoutParams = contentView.getLayoutParams(); + if (layoutParams != null) { + layoutParams.width = width; + layoutParams.height = height; + if (MarginLayoutParams.class.isInstance(layoutParams)) { + int x = left; + int y = top; + + int[] location = new int[2]; + parent.getLocationInWindow(location); + x += location[0]; + y += location[1]; + + MarginLayoutParams marginLayoutParams = MarginLayoutParams.class.cast(layoutParams); + // Clip XY. + View rootView = View.class.cast(contentView.getParent()); + if (rootView != null) { + x = MozcUtil.clamp(x, 0, rootView.getWidth() - width); + y = MozcUtil.clamp(y, 0, rootView.getHeight() - height); + } + + marginLayoutParams.setMargins(x, y, 0, 0); + } + contentView.setLayoutParams(layoutParams); + } + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/ScrollGuideView.java b/src/android/src/com/google/android/inputmethod/japanese/ui/ScrollGuideView.java index 5aff03c6d..a5635a353 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ui/ScrollGuideView.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/ScrollGuideView.java @@ -31,7 +31,7 @@ import org.mozc.android.inputmethod.japanese.resources.R; import org.mozc.android.inputmethod.japanese.view.RectKeyDrawable; -import org.mozc.android.inputmethod.japanese.view.SkinType; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; @@ -51,8 +51,8 @@ public class ScrollGuideView extends View { private final int scrollBarMinimumHeight = getScrollBarMinimumHeight(getResources()); private Optional snapScroller = Optional.absent(); - private SkinType skinType = SkinType.BLUE_LIGHTGRAY; - @VisibleForTesting Drawable scrollBarDrawable = createScrollBarDrawable(skinType); + private Skin skin = Skin.getFallbackInstance(); + @VisibleForTesting Drawable scrollBarDrawable = createScrollBarDrawable(skin); public ScrollGuideView(Context context) { super(context); @@ -66,13 +66,12 @@ public ScrollGuideView(Context context, AttributeSet attributeSet, int defStyle) super(context, attributeSet, defStyle); } - private static Drawable createScrollBarDrawable(SkinType skinType) { + private static Drawable createScrollBarDrawable(Skin skin) { // TODO(hidehiko): Probably we should rename the RectKeyDrawable, // because this usage is not the key but actually we can reuse the code as is. - Preconditions.checkNotNull(skinType); return new RectKeyDrawable(1, 0, 1, 1, - skinType.candidateScrollBarTopColor, - skinType.candidateScrollBarBottomColor, + skin.candidateScrollBarTopColor, + skin.candidateScrollBarBottomColor, 0, 0, 0, 0); } @@ -82,12 +81,15 @@ private static int getScrollBarMinimumHeight(Resources resources) { } /** Sets the skin type, and regenerates an indicator drawable if necessary. */ - public void setSkinType(SkinType skinType) { - if (this.skinType == Preconditions.checkNotNull(skinType)) { + @SuppressWarnings("deprecation") + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + if (this.skin.equals(skin)) { return; } - this.skinType = skinType; - scrollBarDrawable = createScrollBarDrawable(skinType); + this.skin = skin; + scrollBarDrawable = createScrollBarDrawable(skin); + setBackgroundDrawable(skin.scrollBarBackgroundDrawable.getConstantState().newDrawable()); invalidate(); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/SideFrameStubProxy.java b/src/android/src/com/google/android/inputmethod/japanese/ui/SideFrameStubProxy.java index da022601f..226759542 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ui/SideFrameStubProxy.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/SideFrameStubProxy.java @@ -29,22 +29,16 @@ package org.mozc.android.inputmethod.japanese.ui; -import org.mozc.android.inputmethod.japanese.resources.R; -import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory; +import org.mozc.android.inputmethod.japanese.view.Skin; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.GradientDrawable.Orientation; import android.view.View; import android.view.View.OnClickListener; -import android.view.ViewGroup; import android.view.ViewStub; import android.view.ViewStub.OnInflateListener; -import android.view.animation.Animation; import android.widget.FrameLayout; import android.widget.ImageView; @@ -64,35 +58,22 @@ public class SideFrameStubProxy { private int inputFrameHeight = 0; private Optional buttonOnClickListener = Optional.absent(); - private Optional dropshadowShort = Optional.absent(); - private Optional dropshadowLong = Optional.absent(); - private int dropShadowShortVisibility = View.GONE; - private int dropShadowShortLayoutHeight = 0; - private int dropShadowLongLayoutHeight = 0; + private int adjustButtonResourceId; + private Skin skin = Skin.getFallbackInstance(); - private static void setLayoutHeight(View view, int height) { - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - layoutParams.height = height; - view.setLayoutParams(layoutParams); - } + private Resources resources; - private static Drawable createDropShadowGradientDrawable( - int startColor, int endColor, float radius, float centerX, float centerY) { - GradientDrawable gradientDrawable = - new GradientDrawable(Orientation.TL_BR, new int[]{startColor, endColor}); - gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT); - gradientDrawable.setGradientRadius(radius); - gradientDrawable.setGradientCenter(centerX, centerY); - return gradientDrawable; + private void updateAdjustButtonImage() { + Preconditions.checkState(adjustButton.isPresent()); + adjustButton.get().setImageDrawable(skin.getDrawable(resources, adjustButtonResourceId)); } - public void initialize(View view, int stubId, final int dropShadowShortTopId, - final int dropShadowLongTopId, final int adjustButtonId, - final int adjustButtonResouceId, final float dropShadowGradientCenterX, - final int dropshadowShortId, - final int dropshadowLongId) { + public void initialize(View view, int stubId, final int adjustButtonId, + int adjustButtonResourceId) { + this.resources = Preconditions.checkNotNull(view).getResources(); ViewStub viewStub = ViewStub.class.cast(view.findViewById(stubId)); currentView = Optional.of(viewStub); + this.adjustButtonResourceId = adjustButtonResourceId; viewStub.setOnInflateListener(new OnInflateListener() { @@ -103,32 +84,22 @@ public void onInflate(ViewStub stub, View view) { currentView = Optional.of(view); currentView.get().setVisibility(View.VISIBLE); - dropshadowShort = Optional.of(view.findViewById(dropshadowShortId)); - dropshadowLong = Optional.of(view.findViewById(dropshadowLongId)); adjustButton = Optional.of(ImageView.class.cast(view.findViewById(adjustButtonId))); adjustButton.get().setOnClickListener(buttonOnClickListener.orNull()); - Resources resources = view.getResources(); - MozcDrawableFactory drawableFactory = new MozcDrawableFactory(resources); - adjustButton.get().setImageDrawable( - drawableFactory.getDrawable(adjustButtonResouceId).orNull()); - - // Create dropshadow corner drawable. - // Because resource xml cannot set gradientRadius in dip style, code it. - int startColor = resources.getColor(R.color.dropshadow_start); - int endColor = resources.getColor(R.color.dropshadow_end); - float radius = resources.getDimensionPixelSize(R.dimen.translucent_border_height); - Drawable gradientTop = createDropShadowGradientDrawable(startColor, endColor, radius, - dropShadowGradientCenterX, 1.0f); - view.findViewById(dropShadowShortTopId).setBackgroundDrawable(gradientTop); - view.findViewById(dropShadowLongTopId).setBackgroundDrawable(gradientTop); - + adjustButton.get().setLayerType(View.LAYER_TYPE_SOFTWARE, null); + updateAdjustButtonImage(); resetAdjustButtonBottomMarginInternal(inputFrameHeight); - flipDropShadowVisibilityInternal(dropShadowShortVisibility); - setDropShadowHeightInternal(dropShadowShortLayoutHeight, dropShadowLongLayoutHeight); } }); } + public void setSkin(Skin skin) { + this.skin = Preconditions.checkNotNull(skin); + if (adjustButton.isPresent()) { + updateAdjustButtonImage(); + } + } + public void setButtonOnClickListener(OnClickListener onClickListener) { buttonOnClickListener = Optional.of(Preconditions.checkNotNull(onClickListener)); } @@ -141,7 +112,7 @@ public void setFrameVisibility(int visibility) { private void resetAdjustButtonBottomMarginInternal(int inputFrameHeight) { if (adjustButton.isPresent()) { - ImageView imageView = ImageView.class.cast(adjustButton.get()); + ImageView imageView = adjustButton.get(); FrameLayout.LayoutParams layoutParams = FrameLayout.LayoutParams.class.cast( imageView.getLayoutParams()); layoutParams.bottomMargin = (inputFrameHeight - layoutParams.height) / 2; @@ -155,43 +126,4 @@ public void resetAdjustButtonBottomMargin(int inputFrameHeight) { } this.inputFrameHeight = inputFrameHeight; } - - private void flipDropShadowVisibilityInternal(int shortVisibility) { - if (dropshadowShort.isPresent() && dropshadowLong.isPresent()) { - dropshadowShort.get().setVisibility(shortVisibility); - dropshadowLong.get().setVisibility( - shortVisibility == View.VISIBLE ? View.INVISIBLE : View.VISIBLE); - } - } - - public void flipDropShadowVisibility(int shortVisibility) { - if (inflated) { - flipDropShadowVisibilityInternal(shortVisibility); - return; - } - dropShadowShortVisibility = shortVisibility; - } - - private void setDropShadowHeightInternal(int shortHeight, int longHeight) { - if (dropshadowShort.isPresent() && dropshadowLong.isPresent()) { - setLayoutHeight(dropshadowShort.get(), shortHeight); - setLayoutHeight(dropshadowLong.get(), longHeight); - } - } - - public void setDropShadowHeight(int shortHeight, int longHeight) { - if (inflated) { - setDropShadowHeightInternal(shortHeight, longHeight); - return; - } - dropShadowShortLayoutHeight = shortHeight; - dropShadowLongLayoutHeight = longHeight; - } - - public void startDropShadowAnimation(Animation shortAnimation, Animation longAnimation) { - if (inflated && dropshadowShort.isPresent() && dropshadowLong.isPresent()) { - dropshadowShort.get().startAnimation(Preconditions.checkNotNull(shortAnimation)); - dropshadowLong.get().startAnimation(Preconditions.checkNotNull(longAnimation)); - } - } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/ui/SpanFactory.java b/src/android/src/com/google/android/inputmethod/japanese/ui/SpanFactory.java index 0d7c33e23..aab8e227d 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/ui/SpanFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/ui/SpanFactory.java @@ -31,16 +31,14 @@ import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; import org.mozc.android.inputmethod.japanese.ui.CandidateLayout.Span; +import org.mozc.android.inputmethod.japanese.util.CandidateDescriptionUtil; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import android.graphics.Paint; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.StringTokenizer; /** * Factory to create Span instances based on given CandidateWord instances. @@ -65,7 +63,7 @@ public void setDescriptionTextSize(float descriptionTextSize) { } public void setDescriptionDelimiter(String descriptionDelimiter) { - this.descriptionDelimiter = Optional.of(Preconditions.checkNotNull(descriptionDelimiter)); + this.descriptionDelimiter = Optional.of(descriptionDelimiter); } public Span newInstance(CandidateWord candidateWord) { @@ -73,8 +71,8 @@ public Span newInstance(CandidateWord candidateWord) { float valueWidth = valuePaint.measureText(candidateWord.getValue()); String description = candidateWord.getAnnotation().getDescription(); - List splitDescriptionList = splitDescription(Strings.nullToEmpty(description), - descriptionDelimiter); + List splitDescriptionList = CandidateDescriptionUtil.extractDescriptions( + Strings.nullToEmpty(description), descriptionDelimiter); float descriptionWidth = 0; for (String line : splitDescriptionList) { float width = descriptionPaint.measureText(line); @@ -85,39 +83,4 @@ public Span newInstance(CandidateWord candidateWord) { return new Span(Optional.of(candidateWord), valueWidth, descriptionWidth, splitDescriptionList); } - - private static List splitDescription( - String description, Optional descriptionDelimiter) { - if (description.length() == 0) { - // No description is available. - return Collections.emptyList(); - } - - if (!descriptionDelimiter.isPresent()) { - // If the delimiter is not set, return the description as is. - return Collections.singletonList(description); - } - - // Split the description by delimiter. - StringTokenizer tokenizer = new StringTokenizer(description, descriptionDelimiter.get()); - List result = new ArrayList(); - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken(); - if (isEligibleDescriptionFragment(token)) { - result.add(token); - } - } - - return result; - } - - private static boolean isEligibleDescriptionFragment(String descriptionFragment) { - // We'd like to always remove "ひらがな" because the description fragment frequently - // and largely increases the width of a candidate span. - // Increased width reduces the number of the candidates which are shown in a screen. - // This behavior is especially harmful for zero-query suggestion - // because zero-query suggestion mainly shows Hiragana candidates. - // TODO(matsuzakit): Such filtering/oprimization should be done in the server side. - return !"ひらがな".equals(descriptionFragment); - } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryActionBarHelperFactory.java b/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryActionBarHelperFactory.java deleted file mode 100644 index f889f89c1..000000000 --- a/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryActionBarHelperFactory.java +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2010-2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package org.mozc.android.inputmethod.japanese.userdictionary; - -import org.mozc.android.inputmethod.japanese.resources.R; - -import android.app.Activity; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.Menu; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.Window; - -/** - * Utility to support Action Bar like UI for all the devices. - * - * Android 3.0 (or later) supports a new UI named "Action Bar", and it is recommended to be used. - * However, Google Japanese Input supports also Android 2.3 or earlier. - * To provide quite similar user experiences for all the platforms, we implement "mimic" of - * Action Bar for such earlier devices. - * On the other hand, on later devices, we use Action Bar itself (supported by Android OS). - * - * This class provides utilities to fill the gaps between earlier and later devices. - * - */ -class UserDictionaryActionBarHelperFactory { - - /** - * Hook-methods of Activity to support ActionBar like UI for earlier devices. - */ - interface ActionBarHelper { - void onCreate(Bundle savedInstance); - public void onPostCreate( - Bundle savedInstance, - OnClickListener addEntryClickListener, - OnClickListener deleteEntryClickListener, - OnClickListener undoClickListener); - void onConfigurationChanged(Configuration configuration); - void onCreateOptionsMenu(Menu menu); - } - - /** - * Implementation of ActionBar like ui for earlier devices. - */ - private static class MozcActionBarHelper implements ActionBarHelper { - private final Activity activity; - - MozcActionBarHelper(Activity activity) { - this.activity = activity; - } - - @Override - public void onCreate(Bundle savedInstance) { - // We'll use "custom title" for the ActionBar like UI. - activity.getWindow().requestFeature(Window.FEATURE_CUSTOM_TITLE); - } - - @Override - public void onPostCreate(Bundle savedInstance, - OnClickListener addEntryClickListener, - OnClickListener deleteEntryClickListener, - OnClickListener undoClickListener) { - activity.getWindow().setFeatureInt( - Window.FEATURE_CUSTOM_TITLE, - R.layout.user_dictionary_tool_action_bar_view); - - activity.findViewById(R.id.user_dictionary_tool_action_bar_add_entry) - .setOnClickListener(addEntryClickListener); - activity.findViewById(R.id.user_dictionary_tool_split_action_bar_add_entry) - .setOnClickListener(addEntryClickListener); - activity.findViewById(R.id.user_dictionary_tool_action_bar_delete_entry) - .setOnClickListener(deleteEntryClickListener); - activity.findViewById(R.id.user_dictionary_tool_split_action_bar_delete_entry) - .setOnClickListener(deleteEntryClickListener); - - // Need to select either icons (on top action bar, or bottom split action bar) - // should be shown. - updateActionBar(); - } - - @Override - public void onConfigurationChanged(Configuration configuration) { - updateActionBar(); - } - - @Override - public void onCreateOptionsMenu(Menu menu) { - // These items should be put on the self implemented action bar. - menu.findItem(R.id.user_dictionary_tool_menu_add_entry).setVisible(false); - menu.findItem(R.id.user_dictionary_tool_menu_delete_entry).setVisible(false); - } - - private void updateActionBar() { - DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); - if (metrics.widthPixels < 480 * metrics.scaledDensity) { - // Show split action bar. Instead, hide icons on the main action bar. - activity.findViewById(R.id.user_dictionary_tool_action_bar_add_entry) - .setVisibility(View.GONE); - activity.findViewById(R.id.user_dictionary_tool_action_bar_delete_entry) - .setVisibility(View.GONE); - activity.findViewById(R.id.user_dictionary_tool_split_action_bar) - .setVisibility(View.VISIBLE); - } else { - // Hide split action bar. Instead, show icons on the main action bar. - activity.findViewById(R.id.user_dictionary_tool_action_bar_add_entry) - .setVisibility(View.VISIBLE); - activity.findViewById(R.id.user_dictionary_tool_action_bar_delete_entry) - .setVisibility(View.VISIBLE); - activity.findViewById(R.id.user_dictionary_tool_split_action_bar) - .setVisibility(View.GONE); - } - } - } - - /** - * Implement to use system Action Bar (which is supported Android 3.0 or later). - */ - private static class SystemActionBarHelper implements ActionBarHelper { - private final Activity activity; - - SystemActionBarHelper(Activity activity) { - this.activity = activity; - } - - @Override - public void onCreate(Bundle savedInstance) { - activity.getWindow().requestFeature(Window.FEATURE_ACTION_BAR); - } - - @Override - public void onPostCreate(Bundle savedInstance, - OnClickListener addEntryClickListener, - OnClickListener deleteEntryClickListener, - OnClickListener undoClickListener) { - // Disable self implemented action bars. - activity.findViewById(R.id.user_dictionary_tool_split_action_bar).setVisibility(View.GONE); - } - - @Override - public void onConfigurationChanged(Configuration configuration) { - // Do nothing. - } - - @Override - public void onCreateOptionsMenu(Menu menu) { - // Do nothing. - } - } - - private UserDictionaryActionBarHelperFactory() { - } - - static ActionBarHelper newInstance(Activity activity) { - return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) - ? new SystemActionBarHelper(activity) - : new MozcActionBarHelper(activity); - } -} diff --git a/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryToolActivity.java b/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryToolActivity.java index 2bfc8b343..618b40115 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryToolActivity.java +++ b/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryToolActivity.java @@ -37,7 +37,6 @@ import org.mozc.android.inputmethod.japanese.resources.R; import org.mozc.android.inputmethod.japanese.session.SessionExecutor; import org.mozc.android.inputmethod.japanese.session.SessionHandlerFactory; -import org.mozc.android.inputmethod.japanese.userdictionary.UserDictionaryActionBarHelperFactory.ActionBarHelper; import org.mozc.android.inputmethod.japanese.userdictionary.UserDictionaryUtil.DictionaryNameDialog; import org.mozc.android.inputmethod.japanese.userdictionary.UserDictionaryUtil.DictionaryNameDialogListener; import org.mozc.android.inputmethod.japanese.userdictionary.UserDictionaryUtil.WordRegisterDialog; @@ -49,10 +48,8 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.util.SparseBooleanArray; @@ -77,9 +74,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Enumeration; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -158,21 +153,11 @@ public void onClick(View v) { private static final int IMPORT_DICTIONARY_SELECTION_DIALOG_ID = 5; private UserDictionaryToolModel model; - private final ActionBarHelper actionBarHelper = - UserDictionaryActionBarHelperFactory.newInstance(this); private ToastManager toastManager; - private final Set

visibleDialogSet = new HashSet(); - private final OnDismissListener dialogDismissListener = new OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - visibleDialogSet.remove(dialog); - } - }; @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); - actionBarHelper.onCreate(savedInstance); toastManager = new ToastManager(this); // Initialize model. @@ -221,31 +206,6 @@ private void initializeEntryListView() { entryListView.setAdapter(adapter); } - @Override - protected void onPostCreate(Bundle savedInstance) { - super.onPostCreate(savedInstance); - actionBarHelper.onPostCreate( - savedInstance, - new OnClickListener() { - @Override - public void onClick(View v) { - maybeShowAddEntryDialog(); - } - }, - new OnClickListener() { - @Override - public void onClick(View v) { - maybeDeleteEntry(); - } - }, - new OnClickListener() { - @Override - public void onClick(View v) { - runUndo(); - } - }); - } - @Override protected void onDestroy() { // To release pending resources. @@ -263,7 +223,6 @@ protected void onResume() { toastManager.maybeShowMessageShortly(model.resumeSession(defaultDictionaryName)); updateDictionaryNameSpinner(); updateEntryList(); - updateDialogWindowSize(); } @Override @@ -375,19 +334,11 @@ protected void onPause() { super.onPause(); } - @Override - public void onConfigurationChanged(Configuration configuration) { - super.onConfigurationChanged(configuration); - actionBarHelper.onConfigurationChanged(configuration); - updateDialogWindowSize(); - } - // Menu implementation. @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.user_dictionary_tool_menu, menu); - actionBarHelper.onCreateOptionsMenu(menu); return true; } @@ -558,8 +509,7 @@ public Status onPositiveButtonClicked(String word, String reading, PosType pos) } return status; } - }, - dialogDismissListener, toastManager); + }, toastManager); case EDIT_ENTRY_DIALOG_ID: return UserDictionaryUtil.createWordRegisterDialog( @@ -574,8 +524,7 @@ public Status onPositiveButtonClicked(String word, String reading, PosType pos) } return status; } - }, - dialogDismissListener, toastManager); + }, toastManager); case CREATE_DICTIONARY_DIALOG_ID: return UserDictionaryUtil.createDictionaryNameDialog( @@ -591,8 +540,7 @@ public Status onPositiveButtonClicked(String dictionaryName) { } return status; } - }, - dialogDismissListener, toastManager); + }, toastManager); case RENAME_DICTIONARY_DIALOG_ID: return UserDictionaryUtil.createDictionaryNameDialog( @@ -607,8 +555,7 @@ public Status onPositiveButtonClicked(String dictionaryName) { } return status; } - }, - dialogDismissListener, toastManager); + }, toastManager); case ZIP_FILE_SELECTION_DIALOG_ID: return UserDictionaryUtil.createZipFileSelectionDialog( @@ -648,8 +595,7 @@ public void onClick(DialogInterface dialog, int which) { public void onCancel(DialogInterface dialog) { model.resetImportState(); } - }, - dialogDismissListener); + }); case IMPORT_DICTIONARY_SELECTION_DIALOG_ID: return UserDictionaryUtil.createImportDictionarySelectionDialog( @@ -684,8 +630,7 @@ public void onClick(DialogInterface dialog, int which) { public void onCancel(DialogInterface dialog) { model.resetImportState(); } - }, - dialogDismissListener); + }); } MozcLog.e("Unknown Dialog ID: " + id); @@ -696,11 +641,6 @@ public void onCancel(DialogInterface dialog) { @Override protected void onPrepareDialog(int id, Dialog dialog) { super.onPrepareDialog(id, dialog); - - // Set dialog window size based on the display size. - UserDictionaryUtil.setDialogWindowSize(dialog); - visibleDialogSet.add(dialog); - switch (id) { case ADD_ENTRY_DIALOG_ID: WordRegisterDialog.class.cast(dialog).setEntry(Entry.newBuilder() @@ -784,12 +724,6 @@ private void updateEntryList() { ArrayAdapter.class.cast(entryList.getAdapter()).notifyDataSetChanged(); } - private void updateDialogWindowSize() { - for (Dialog dialog : visibleDialogSet) { - UserDictionaryUtil.setDialogWindowSize(dialog); - } - } - private Spinner getDictionaryNameSpinner() { return Spinner.class.cast(findViewById(R.id.user_dictionary_tool_dictionary_name_spinner)); } diff --git a/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryUtil.java b/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryUtil.java index 1056c92f9..422b61007 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryUtil.java +++ b/src/android/src/com/google/android/inputmethod/japanese/userdictionary/UserDictionaryUtil.java @@ -40,24 +40,18 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.Message; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.view.WindowManager; -import android.view.WindowManager.LayoutParams; import android.view.inputmethod.InputMethodManager; import android.widget.ArrayAdapter; import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; @@ -111,7 +105,6 @@ private static class UserDictionaryBaseDialog extends AlertDialog { UserDictionaryBaseDialog(Context context, int titleResourceId, int viewResourceId, UserDictionaryBaseDialogListener listener, - OnDismissListener dismissListener, ToastManager toastManager) { super(context); this.listener = listener; @@ -130,28 +123,12 @@ private static class UserDictionaryBaseDialog extends AlertDialog { setButton(DialogInterface.BUTTON_NEGATIVE, context.getText(android.R.string.cancel), DialogInterface.OnClickListener.class.cast(null)); setCancelable(true); - setOnDismissListener(dismissListener); } @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); - ViewGroup contentGroup = ViewGroup.class.cast(findViewById(android.R.id.content)); - if (contentGroup != null && contentGroup.getChildCount() > 0) { - // Wrap the content view by ScrollView so that we can scroll the dialog window - // in order to avoid shrinking the main edit text fields. - ScrollView view = ScrollView.class.cast( - LayoutInflater.from(getContext()).inflate( - R.layout.user_dictionary_tool_empty_scrollview, null)); - View contentView = contentGroup.getChildAt(0); - contentGroup.removeViewAt(0); - FrameLayout.class.cast( - view.findViewById(R.id.user_dictionary_tool_empty_scroll_view_content)) - .addView(contentView); - contentGroup.addView(view, 0); - } - // To override the default behavior that the dialog is dismissed after user's clicking // a button regardless of any action inside listener, we set the callback directly // to the button and manage dismissing behavior. @@ -262,7 +239,6 @@ interface WordRegisterDialogListener { static class WordRegisterDialog extends UserDictionaryBaseDialog { WordRegisterDialog(Context context, int titleResourceId, final WordRegisterDialogListener listener, - OnDismissListener dismissListener, ToastManager toastManager) { super(context, titleResourceId, R.layout.user_dictionary_tool_word_register_dialog_view, new UserDictionaryBaseDialogListener() { @@ -273,8 +249,7 @@ public Status onPositiveButtonClicked(View view) { getText(view, R.id.user_dictionary_tool_word_register_dialog_reading), getPos(view, R.id.user_dictionary_tool_word_register_dialog_pos)); } - }, - dismissListener, toastManager); + }, toastManager); // TODO(hidehiko): Attach a callback for un-focused event on "word" EditText. // and invoke reverse conversion (if necessary) to fill reading automatically. } @@ -320,7 +295,6 @@ interface DictionaryNameDialogListener { static class DictionaryNameDialog extends UserDictionaryBaseDialog { DictionaryNameDialog(Context context, int titleResourceId, final DictionaryNameDialogListener listener, - OnDismissListener dismissListener, ToastManager toastManager) { super(context, titleResourceId, R.layout.user_dictionary_tool_dictionary_name_dialog_view, new UserDictionaryBaseDialogListener() { @@ -329,8 +303,7 @@ public Status onPositiveButtonClicked(View view) { return listener.onPositiveButtonClicked( getText(view, R.id.user_dictionary_tool_dictionary_name_dialog_name)); } - }, - dismissListener, toastManager); + }, toastManager); } /** @@ -345,8 +318,6 @@ void setDictionaryName(String dictionaryName) { } } - private static final int DIALOG_WIDTH_THRESHOLD = 480; // in dip. - /** A map from PosType to the string resource id for i18n. */ private static final Map POS_RESOURCE_MAP; static { @@ -518,9 +489,8 @@ static int getPosStringResourceIdForDictionaryExport(PosType pos) { */ static WordRegisterDialog createWordRegisterDialog( Context context, int titleResourceId, WordRegisterDialogListener listener, - OnDismissListener dismissListener, ToastManager toastManager) { - return new WordRegisterDialog( - context, titleResourceId, listener, dismissListener, toastManager); + ToastManager toastManager) { + return new WordRegisterDialog(context, titleResourceId, listener, toastManager); } /** @@ -528,9 +498,8 @@ static WordRegisterDialog createWordRegisterDialog( */ static DictionaryNameDialog createDictionaryNameDialog( Context context, int titleResourceId, DictionaryNameDialogListener listener, - OnDismissListener dismissListener, ToastManager toastManager) { - return new DictionaryNameDialog( - context, titleResourceId, listener, dismissListener, toastManager); + ToastManager toastManager) { + return new DictionaryNameDialog(context, titleResourceId, listener, toastManager); } /** @@ -540,11 +509,10 @@ static Dialog createZipFileSelectionDialog( Context context, int titleResourceId, DialogInterface.OnClickListener positiveButtonListener, DialogInterface.OnClickListener negativeButtonListener, - DialogInterface.OnCancelListener cancelListener, - OnDismissListener dismissListener) { + DialogInterface.OnCancelListener cancelListener) { return createSimpleSpinnerDialog( context, titleResourceId, positiveButtonListener, negativeButtonListener, - cancelListener, dismissListener); + cancelListener); } /** @@ -554,30 +522,26 @@ static Dialog createImportDictionarySelectionDialog( Context context, int titleResourceId, DialogInterface.OnClickListener positiveButtonListener, DialogInterface.OnClickListener negativeButtonListener, - DialogInterface.OnCancelListener cancelListener, - OnDismissListener dismissListener) { + DialogInterface.OnCancelListener cancelListener) { return createSimpleSpinnerDialog( - context, titleResourceId, positiveButtonListener, negativeButtonListener, - cancelListener, dismissListener); + context, titleResourceId, positiveButtonListener, negativeButtonListener, cancelListener); } private static Dialog createSimpleSpinnerDialog( Context context, int titleResourceId, DialogInterface.OnClickListener positiveButtonListener, DialogInterface.OnClickListener negativeButtonListener, - DialogInterface.OnCancelListener cancelListener, - OnDismissListener dismissListener) { + DialogInterface.OnCancelListener cancelListener) { View view = LayoutInflater.from(context).inflate( R.layout.user_dictionary_tool_simple_spinner_dialog_view, null); AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(titleResourceId) .setView(view) - .setPositiveButton(android.R.string.yes, positiveButtonListener) + .setPositiveButton(android.R.string.ok, positiveButtonListener) .setNegativeButton(android.R.string.cancel, negativeButtonListener) .setOnCancelListener(cancelListener) .setCancelable(true) .create(); - dialog.setOnDismissListener(dismissListener); return dialog; } @@ -588,31 +552,6 @@ static void showInputMethod(Dialog dialog) { dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } - /** - * The default size of dialog looks not good on some devices, such as tablets. - * So, modify the dialog size to make the look better. - * - * The current strategy is: - * - if the display is small, we tries to fill the display width by the dialog. - * - otherwise, set w480dip to the window. - */ - static void setDialogWindowSize(Dialog dialog) { - DisplayMetrics metrics = dialog.getContext().getResources().getDisplayMetrics(); - LayoutParams params = dialog.getWindow().getAttributes(); - float dialogWidthThreshold = DIALOG_WIDTH_THRESHOLD * metrics.scaledDensity; - if (metrics.widthPixels > dialogWidthThreshold) { - params.width = (int) dialogWidthThreshold; - } else { - params.width = LayoutParams.MATCH_PARENT; - } - if (dialog instanceof UserDictionaryBaseDialog) { - // If the ScrollView hack is used for the dialog to make it scrollable, - // it is necessary to set its height parameter MATCH_PARENT as a part of the hack. - params.height = LayoutParams.MATCH_PARENT; - } - dialog.getWindow().setAttributes(params); - } - /** * Returns the {@code String} instance with detecting the Japanese encoding. * @throws UnsupportedEncodingException if it fails to detect the encoding. diff --git a/src/android/src/com/google/android/inputmethod/japanese/util/CandidateDescriptionUtil.java b/src/android/src/com/google/android/inputmethod/japanese/util/CandidateDescriptionUtil.java new file mode 100644 index 000000000..0f17cca20 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/util/CandidateDescriptionUtil.java @@ -0,0 +1,139 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.util; + +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +/** + * Utility to handle candidate description. + */ +public class CandidateDescriptionUtil { + + private static final Set DESCRIPTION_BLACKLIST_SET; + static { + String[] blacklist = new String[] { + "ひらがな", + "数字", + "丸数字", + "大字", + "絵文字", + "顔文字", + "<機種依存>", + "捨て仮名", + }; + DESCRIPTION_BLACKLIST_SET = Collections.unmodifiableSet( + new HashSet(Arrays.asList(blacklist))); + } + + private static final String[] DESCRIPTION_SUFFIX_BLACKLIST = new String[] { + "の旧字体", + "の簡易慣用字体", + "の印刷標準字体", + "の俗字", + "の正字", + "の本字", + "の異体字", + "の略字", + "の別字", + }; + + private static final Map DESCRIPTION_SHORTEN_MAP; + static { + Map map = new HashMap(); + map.put("小書き文字", "小書き"); + map.put("ローマ数字(大文字)", "ローマ数字"); + map.put("ローマ数字(小文字)", "ローマ数字"); + DESCRIPTION_SHORTEN_MAP = Collections.unmodifiableMap(map); + } + + private CandidateDescriptionUtil() {} + + public static List extractDescriptions( + String description, Optional descriptionDelimiter) { + Preconditions.checkNotNull(description); + Preconditions.checkNotNull(descriptionDelimiter); + + if (description.length() == 0) { + // No description is available. + return Collections.emptyList(); + } + + if (!descriptionDelimiter.isPresent()) { + // If the delimiter is not set, return the description as is. + return Collections.singletonList(description); + } + + // Split the description by delimiter. + StringTokenizer tokenizer = new StringTokenizer(description, descriptionDelimiter.get()); + List result = new ArrayList(); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (isEligibleDescriptionFragment(token)) { + result.add(shortenDescriptionFragment(token)); + } + } + + return result; + } + + private static boolean isEligibleDescriptionFragment(String descriptionFragment) { + // We'd like to always remove some descriptions because the description fragment frequently + // and largely increases the width of a candidate span. + // Increased width reduces the number of the candidates which are shown in a screen. + // TODO(matsuzakit): Such filtering/optimization should be done in the server side. + if (DESCRIPTION_BLACKLIST_SET.contains(descriptionFragment)) { + return false; + } + for (String suffix : DESCRIPTION_SUFFIX_BLACKLIST) { + if (descriptionFragment.endsWith(suffix)) { + return false; + } + } + return true; + } + + private static String shortenDescriptionFragment(String descriptionFragment) { + Preconditions.checkNotNull(descriptionFragment); + return Objects.firstNonNull(DESCRIPTION_SHORTEN_MAP.get(descriptionFragment), + descriptionFragment); + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/util/ImeSwitcherFactory.java b/src/android/src/com/google/android/inputmethod/japanese/util/ImeSwitcherFactory.java index 2e924b227..3080f90fd 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/util/ImeSwitcherFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/util/ImeSwitcherFactory.java @@ -62,8 +62,6 @@ */ public class ImeSwitcherFactory { - static final int SWITCH_NEXT_TARGTET_API_LEVEL = 16; - private static final String GOOGLE_PACKAGE_ID_PREFIX = "com.google.android"; private static final String VOICE_IME_MODE = "voice"; @@ -133,6 +131,13 @@ public interface ImeSwitcher { * @return true if the switching succeeds */ boolean switchToNextInputMethod(boolean onlyCurrentIme); + + /** + * @see InputMethodManager#shouldOfferSwitchingToNextInputMethod(IBinder) + * + * If not supported the API, returns false. + */ + boolean shouldOfferSwitchingToNextInputMethod(); } /** @@ -222,15 +227,20 @@ public boolean switchToVoiceIme(String locale) { public boolean switchToNextInputMethod(boolean onlyCurrentIme) { return false; } + + @Override + public boolean shouldOfferSwitchingToNextInputMethod() { + return false; + } } /** * A switcher for much later OS where switchToNextInputMethod is available. */ - @TargetApi(SWITCH_NEXT_TARGTET_API_LEVEL) - static class NextInputSwitchableImeSwitcher extends SubtypeImeSwitcher { + @TargetApi(16) + static class ImeSwitcher16 extends SubtypeImeSwitcher { - public NextInputSwitchableImeSwitcher(InputMethodService inputMethodService) { + public ImeSwitcher16(InputMethodService inputMethodService) { super(inputMethodService); } @@ -241,14 +251,33 @@ public boolean switchToNextInputMethod(boolean onlyCurrentIme) { } } + /** + * A switcher for much later OS where switchToNextInputMethod is available. + */ + @TargetApi(19) + static class ImeSwitcher21 extends ImeSwitcher16 { + + public ImeSwitcher21(InputMethodService inputMethodService) { + super(inputMethodService); + } + + @Override + public boolean shouldOfferSwitchingToNextInputMethod() { + return MozcUtil.getInputMethodManager(inputMethodService) + .shouldOfferSwitchingToNextInputMethod(getToken()); + } + } + // A constructor of concrete switcher class. // Null if reflection fails. static final Constructor switcherConstructor; static { Class clazz; - if (Build.VERSION.SDK_INT >= SWITCH_NEXT_TARGTET_API_LEVEL) { - clazz = NextInputSwitchableImeSwitcher.class; + if (Build.VERSION.SDK_INT >= 21) { + clazz = ImeSwitcher21.class; + } else if (Build.VERSION.SDK_INT >= 16) { + clazz = ImeSwitcher16.class; } else { clazz = SubtypeImeSwitcher.class; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/util/LauncherIconManagerFactory.java b/src/android/src/com/google/android/inputmethod/japanese/util/LauncherIconManagerFactory.java new file mode 100644 index 000000000..0f77fbb94 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/util/LauncherIconManagerFactory.java @@ -0,0 +1,139 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.util; + +import org.mozc.android.inputmethod.japanese.LauncherActivity; +import org.mozc.android.inputmethod.japanese.MozcUtil; +import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.preference.PreferenceManager; + +/** + * Manager of launcher icon's visibility. + */ +public class LauncherIconManagerFactory { + + /** + * Interface for the manager. + */ + public interface LauncherIconManager { + /** + * Updates launcher icon's visibility by checking the value in preferences or + * by checking whether the app is (updated) system application or not. + * + * @param context The application's context. + */ + public void updateLauncherIconVisibility(Context context); + } + + @VisibleForTesting + static class DefaultImplementation implements LauncherIconManager { + + @Override + public void updateLauncherIconVisibility(Context context) { + Preconditions.checkNotNull(context); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean visible = shouldLauncherIconBeVisible(context, sharedPreferences); + updateComponentEnableSetting(context, LauncherActivity.class, visible); + sharedPreferences.edit() + .putBoolean(PreferenceUtil.PREF_LAUNCHER_ICON_VISIBILITY_KEY, visible) + .apply(); + } + + /** + * Enables/disables component. + * + * @param context The application's context + * @param component The component to be enabled/disabled + * @param enabled true for enabled, false for disabled + */ + private void updateComponentEnableSetting( + Context context, Class component, boolean enabled) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(component); + PackageManager packageManager = context.getPackageManager(); + ComponentName componentName = new ComponentName(context.getApplicationContext(), component); + int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + if (newState != packageManager.getComponentEnabledSetting(componentName)) { + packageManager.setComponentEnabledSetting( + componentName, newState, PackageManager.DONT_KILL_APP); + } + } + + @VisibleForTesting + static boolean shouldLauncherIconBeVisible(Context context, + SharedPreferences sharedPreferences) { + // NOTE: Both flags below can be true at the same time. + boolean isSystemApplication = MozcUtil.isSystemApplication(context); + boolean isUpdatedSystemApplication = MozcUtil.isUpdatedSystemApplication(context); + if (sharedPreferences.contains(PreferenceUtil.PREF_LAUNCHER_ICON_VISIBILITY_KEY)) { + return sharedPreferences.getBoolean( + PreferenceUtil.PREF_LAUNCHER_ICON_VISIBILITY_KEY, true); + } else { + // If PREF_LAUNCHER_ICON_VISIBILITY_KEY is not set, we don't show launcher icon in + // following conditions: + if (isSystemApplication && !isUpdatedSystemApplication) { + // System app (not updated) doesn't show the icon. + return false; + } + if (isUpdatedSystemApplication + && sharedPreferences.contains( + PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE)) { + // Workaround for updated system app from preinstalled 2.16.1955.3. + // Preinstalled 2.16.1955.3 doesn't put PREF_LAUNCHER_ICON_VISIBILITY_KEY + // unless preference screen is shown so checking PREF_LAUNCHER_ICON_VISIBILITY_KEY + // doesn't work for the version. + // However PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE is always written, + // use the preference instaed. + // If the preference exists, this means the IME has been launched as a preinstall + // app at least once. + // Therefore we should take over the visibility (== hide the icon). + return false; + } + return true; + } + } + } + + private static LauncherIconManager defaultInstance = new DefaultImplementation(); + + private LauncherIconManagerFactory() {} + + public static LauncherIconManager getDefaultInstance() { + return defaultInstance; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/util/ParserUtil.java b/src/android/src/com/google/android/inputmethod/japanese/util/ParserUtil.java new file mode 100644 index 000000000..ebfa8da3f --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/util/ParserUtil.java @@ -0,0 +1,98 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.util; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * Utility for XML parser. + */ +public class ParserUtil { + + public static void ignoreWhiteSpaceAndComment(XmlPullParser parser) + throws XmlPullParserException, IOException { + int event = parser.getEventType(); + while (event == XmlPullParser.IGNORABLE_WHITESPACE || event == XmlPullParser.COMMENT) { + event = parser.next(); + } + } + + public static void assertStartDocument(XmlPullParser parser) throws XmlPullParserException { + if (parser.getEventType() != XmlPullParser.START_DOCUMENT) { + throw new IllegalArgumentException( + "The start of document is expected, but actually not: " + + parser.getPositionDescription()); + } + } + + public static void assertEndDocument(XmlPullParser parser) throws XmlPullParserException { + if (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + throw new IllegalArgumentException( + "The end of document is expected, but actually not: " + parser.getPositionDescription()); + } + } + + public static void assertNotEndDocument(XmlPullParser parser) throws XmlPullParserException { + if (parser.getEventType() == XmlPullParser.END_DOCUMENT) { + throw new IllegalArgumentException( + "Unexpected end of document is found: " + parser.getPositionDescription()); + } + } + + public static void assertTagName(XmlPullParser parser, String expectedName) { + String actualName = parser.getName(); + if (!actualName.equals(expectedName)) { + throw new IllegalArgumentException( + "Tag <" + expectedName + "> is expected, but found <" + actualName + ">: " + + parser.getPositionDescription()); + } + } + + public static void assertStartTag(XmlPullParser parser, String expectedName) + throws XmlPullParserException { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalArgumentException( + "Start tag <" + expectedName + "> is expected: " + parser.getPositionDescription()); + } + assertTagName(parser, expectedName); + } + + public static void assertEndTag(XmlPullParser parser, String expectedName) + throws XmlPullParserException { + if (parser.getEventType() != XmlPullParser.END_TAG) { + throw new IllegalArgumentException( + "End tag is expected: " + parser.getPositionDescription()); + } + assertTagName(parser, expectedName); + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/DrawableCache.java b/src/android/src/com/google/android/inputmethod/japanese/view/DrawableCache.java index 5e978f3dd..e679c3447 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/DrawableCache.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/DrawableCache.java @@ -29,10 +29,10 @@ package org.mozc.android.inputmethod.japanese.view; -import org.mozc.android.inputmethod.japanese.MozcLog; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.util.SparseArray; @@ -43,21 +43,21 @@ public class DrawableCache { private final SparseArray cacheMap = new SparseArray(128); - private final MozcDrawableFactory mozcDrawableFactory; - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; + private Skin skin = Skin.getFallbackInstance(); + private final Resources resources; - public DrawableCache(MozcDrawableFactory mozcDrawableFactory) { - this.mozcDrawableFactory = Preconditions.checkNotNull(mozcDrawableFactory); + public DrawableCache(Resources resources) { + this.resources = Preconditions.checkNotNull(resources); } - public void setSkinType(SkinType skinType) { - if (this.skinType == Preconditions.checkNotNull(skinType)) { + public void setSkin(Skin skin) { + Preconditions.checkNotNull(skin); + if (this.skin.equals(skin)) { return; } - this.skinType = skinType; + this.skin = skin; cacheMap.clear(); - mozcDrawableFactory.setSkinType(skinType); } /** @@ -74,12 +74,8 @@ public Optional getDrawable(int resourceId) { Integer key = Integer.valueOf(resourceId); Optional drawable = Optional.fromNullable(cacheMap.get(key)); if (!drawable.isPresent()) { - drawable = mozcDrawableFactory.getDrawable(resourceId); - if (drawable.isPresent()) { - cacheMap.put(key, drawable.get()); - } else { - MozcLog.e("Failed to load: " + resourceId); - } + drawable = Optional.of(skin.getDrawable(resources, resourceId)); + cacheMap.put(key, drawable.get()); } return drawable; } diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/DummyDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/DummyDrawable.java new file mode 100644 index 000000000..c9ea33738 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/view/DummyDrawable.java @@ -0,0 +1,82 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.view; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; + +/** + * Singleton dummy drawable which does nothing. + *

+ * Typically used when skin is not specified. + */ +public class DummyDrawable extends Drawable { + + private static final DummyDrawable theInstance = new DummyDrawable(); + + public static DummyDrawable getInstance() { + return theInstance; + } + + private DummyDrawable() {} + + @Override + public void draw(Canvas canvas) { + } + + @Override + public int getOpacity() { + return 0; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public ConstantState getConstantState() { + return new ConstantState() { + @Override + public Drawable newDrawable() { + return theInstance; + } + + @Override + public int getChangingConfigurations() { + return 0; + } + }; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/LightIconDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/LightIconDrawable.java deleted file mode 100644 index 1930cb13e..000000000 --- a/src/android/src/com/google/android/inputmethod/japanese/view/LightIconDrawable.java +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2010-2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package org.mozc.android.inputmethod.japanese.view; - -import android.graphics.BlurMaskFilter; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.BlurMaskFilter.Blur; -import android.graphics.Paint.Style; -import android.graphics.Shader.TileMode; -import android.graphics.drawable.Drawable; - -/** - * The light (indicator) mark for ALT key on qwerty layout. - * - */ -public class LightIconDrawable extends Drawable { - private final float topOffset; - private final float rightOffset; - - private final int lightColor; - private final int darkColor; - private final float radius; - - private float centerX; - private float centerY; - - private final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint shadePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - - public LightIconDrawable(float topOffset, float rightOffset, - int lightColor, int darkColor, int shadeColor, float radius) { - this.topOffset = topOffset; - this.rightOffset = rightOffset; - this.lightColor = lightColor; - this.darkColor = darkColor; - this.radius = radius; - - if (lightColor == darkColor) { - basePaint.setColor(lightColor); - } - - shadePaint.setStyle(Style.STROKE); - shadePaint.setStrokeWidth(1); - shadePaint.setMaskFilter(new BlurMaskFilter(2, Blur.INNER)); - shadePaint.setColor(shadeColor); - } - - /* (non-Javadoc) - * @see android.graphics.drawable.Drawable#draw(android.graphics.Canvas) - */ - @Override - public void draw(Canvas canvas) { - canvas.drawCircle(centerX, centerY, radius, basePaint); - canvas.drawCircle(centerX, centerY, radius, shadePaint); - } - - @Override - protected void onBoundsChange(Rect bound) { - super.onBoundsChange(bound); - - centerX = bound.right - rightOffset; - centerY = bound.top + topOffset; - - if (lightColor != darkColor) { - float top = centerY - radius; - // Hard coded parameters are based on design mock. - basePaint.setShader(new LinearGradient( - centerX, top, centerX + 2, top + 8, lightColor, darkColor, TileMode.CLAMP)); - } - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void setAlpha(int alpha) { - } - - @Override - public void setColorFilter(ColorFilter cf) { - } -} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/MozcDrawableFactory.java b/src/android/src/com/google/android/inputmethod/japanese/view/MozcDrawableFactory.java index abb0aa317..4a66552a5 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/MozcDrawableFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/MozcDrawableFactory.java @@ -31,14 +31,17 @@ import org.mozc.android.inputmethod.japanese.MozcLog; import org.mozc.android.inputmethod.japanese.MozcUtil; +import com.google.common.base.Charsets; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Paint.Align; import android.graphics.Paint.Cap; import android.graphics.Paint.Join; import android.graphics.Paint.Style; @@ -48,13 +51,16 @@ import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Shader.TileMode; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.PictureDrawable; import android.graphics.drawable.StateListDrawable; +import android.os.Build; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Locale; /** * Factory to create Drawables from raw resources which is in mozc original format. @@ -68,7 +74,16 @@ * Also, for performance purpose, this class caches the parsed drawable. * */ -public class MozcDrawableFactory { +class MozcDrawableFactory { + + private static class MozcStyle { + Paint paint = new Paint(); + int dominantBaseline = COMMAND_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO; + } + + /** Locale field for {@link Paint#setTextLocale(Locale)}. */ + private static final Optional TEXT_LOCALE = (Build.VERSION.SDK_INT >= 17) + ? Optional.of(Locale.JAPAN) : Optional.absent(); private static final int DRAWABLE_PICTURE = 1; private static final int DRAWABLE_STATE_LIST = 2; @@ -83,6 +98,7 @@ public class MozcDrawableFactory { private static final int COMMAND_PICTURE_DRAW_ELLIPSE = 7; private static final int COMMAND_PICTURE_DRAW_GROUP_START = 8; private static final int COMMAND_PICTURE_DRAW_GROUP_END = 9; + private static final int COMMAND_PICTURE_DRAW_TEXT = 10; private static final int COMMAND_PICTURE_PATH_EOP = 0; private static final int COMMAND_PICTURE_PATH_MOVE = 1; @@ -101,31 +117,42 @@ public class MozcDrawableFactory { private static final int COMMAND_PICTURE_PAINT_STROKE_CAP = 5; private static final int COMMAND_PICTURE_PAINT_STROKE_JOIN = 6; private static final int COMMAND_PICTURE_PAINT_SHADER = 7; + private static final int COMMAND_PICTURE_PAINT_FONT_SIZE = 8; + private static final int COMMAND_PICTURE_PAINT_TEXT_ANCHOR = 9; + private static final int COMMAND_PICTURE_PAINT_DOMINANT_BASELINE = 10; + private static final int COMMAND_PICTURE_PAINT_FONT_WEIGHT = 11; + + private static final int COMMAND_PICTURE_PAINT_TEXT_ANCHOR_START = 0; + private static final int COMMAND_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE = 1; + private static final int COMMAND_PICTURE_PAINT_TEXT_ANCHOR_END = 2; + + private static final int COMMAND_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO = 0; + @SuppressWarnings("unused") + private static final int COMMAND_PICTURE_PAINT_DOMINANTE_BASELINE_CENTRAL = 1; + + @SuppressWarnings("unused") + private static final int COMMAND_PICTURE_PAINT_FONT_WEIGHT_NORMAL = 0; + private static final int COMMAND_PICTURE_PAINT_FONT_WEIGHT_BOLD = 1; private static final int COMMAND_PICTURE_SHADER_LINEAR_GRADIENT = 1; private static final int COMMAND_PICTURE_SHADER_RADIAL_GRADIENT = 2; private static final int[] EMPTY_STATE_LIST = {}; + private static final String FONT_PATH = "subset_font.otf"; + private final Resources resources; private final WeakDrawableCache cacheMap = new WeakDrawableCache(); - private SkinType skinType = SkinType.ORANGE_LIGHTGRAY; + private final Skin skin; + private static volatile Optional typeface = Optional.absent(); - public MozcDrawableFactory(Resources resources) { + MozcDrawableFactory(Resources resources, Skin skin) { this.resources = Preconditions.checkNotNull(resources); + this.skin = Preconditions.checkNotNull(skin); + ensureTypeface(resources.getAssets()); } - // TODO(hidehiko): Add test to make sure getDrawable returns diffenent instances for - // the same resourceId when the current skinType gets different. - public void setSkinType(SkinType skinType) { - if (this.skinType != Preconditions.checkNotNull(skinType)) { - this.skinType = skinType; - // Invalidate cache. - cacheMap.clear(); - } - } - - public Optional getDrawable(int resourceId) { + Optional getDrawable(int resourceId) { if (!resources.getResourceTypeName(resourceId).equalsIgnoreCase("raw")) { // For non-"raw" resources, just delegate loading to Resources. return Optional.fromNullable(resources.getDrawable(resourceId)); @@ -138,7 +165,7 @@ public Optional getDrawable(int resourceId) { try { boolean success = false; try { - drawable = createDrawable(new DataInputStream(stream), skinType); + drawable = createDrawable(new DataInputStream(stream), skin); success = true; } finally { MozcUtil.close(stream, !success); @@ -154,17 +181,16 @@ public Optional getDrawable(int resourceId) { return drawable; } - private static Optional createDrawable(DataInputStream stream, SkinType skinType) + private static Optional createDrawable(DataInputStream stream, Skin skin) throws IOException { Preconditions.checkNotNull(stream); - Preconditions.checkNotNull(skinType); byte tag = stream.readByte(); switch (tag) { case DRAWABLE_PICTURE: - return Optional.of(createPictureDrawable(stream, skinType)); + return Optional.of(createPictureDrawable(stream, skin)); case DRAWABLE_STATE_LIST: - return Optional.of(createStateListDrawable(stream, skinType)); + return Optional.of(createStateListDrawable(stream, skin)); default: MozcLog.e("Unknown tag: " + tag); } @@ -174,16 +200,18 @@ private static Optional createDrawable(DataInputStream stream, SkinTyp // Note, PictureDrawable may cause runtime slowness. // Instead, we can prepare pre-rendered bit-map drawable, by modifying the interface to take // the drawable, which should be faster theoretically. - private static PictureDrawable createPictureDrawable(DataInputStream stream, SkinType skinType) + private static PictureDrawable createPictureDrawable(DataInputStream stream, Skin skin) throws IOException { + Preconditions.checkNotNull(stream); + Preconditions.checkNotNull(skin); // The first eight bytes are width and height (four bytes for each). int width = stream.readUnsignedShort(); int height = stream.readUnsignedShort(); Picture picture = new Picture(); Canvas canvas = picture.beginRecording(width, height); - Paint paint = new Paint(); - resetPaint(paint); + MozcStyle style = new MozcStyle(); + resetStyle(style); LOOP: while (true) { byte command = stream.readByte(); @@ -195,13 +223,13 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski Path path = createPath(stream); int size = stream.readByte(); if (size == 0) { - resetPaint(paint); - canvas.drawPath(path, paint); + resetStyle(style); + canvas.drawPath(path, style.paint); } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); - canvas.drawPath(path, paint); + resetStyle(style); + parseStyle(stream, skin, style); + canvas.drawPath(path, style.paint); } } break; @@ -218,16 +246,17 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski int size = stream.readByte(); if (size == 0) { - resetPaint(paint); + resetStyle(style); for (int i = 0; i < length - 2; i += 2) { - canvas.drawLine(points[i], points[i + 1], points[i + 2], points[i + 3], paint); + canvas.drawLine(points[i], points[i + 1], points[i + 2], points[i + 3], style.paint); } } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); + resetStyle(style); + parseStyle(stream, skin, style); for (int j = 0; j < length - 2; j += 2) { - canvas.drawLine(points[j], points[j + 1], points[j + 2], points[j + 3], paint); + canvas.drawLine(points[j], points[j + 1], points[j + 2], points[j + 3], + style.paint); } } } @@ -254,13 +283,13 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski int size = stream.readUnsignedByte(); if (size == 0) { - resetPaint(paint); - canvas.drawPath(path, paint); + resetStyle(style); + canvas.drawPath(path, style.paint); } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); - canvas.drawPath(path, paint); + resetStyle(style); + parseStyle(stream, skin, style); + canvas.drawPath(path, style.paint); } } break; @@ -273,13 +302,13 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski int size = stream.readUnsignedByte(); if (size == 0) { - resetPaint(paint); - canvas.drawLine(x1, y1, x2, y2, paint); + resetStyle(style); + canvas.drawLine(x1, y1, x2, y2, style.paint); } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); - canvas.drawLine(x1, y1, x2, y2, paint); + resetStyle(style); + parseStyle(stream, skin, style); + canvas.drawLine(x1, y1, x2, y2, style.paint); } } break; @@ -292,13 +321,13 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski int size = stream.readUnsignedByte(); if (size == 0) { - resetPaint(paint); - canvas.drawRect(x, y, x + w, y + h, paint); + resetStyle(style); + canvas.drawRect(x, y, x + w, y + h, style.paint); } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); - canvas.drawRect(x, y, x + w, y + h, paint); + resetStyle(style); + parseStyle(stream, skin, style); + canvas.drawRect(x, y, x + w, y + h, style.paint); } } break; @@ -311,13 +340,13 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski int size = stream.readUnsignedByte(); if (size == 0) { - resetPaint(paint); - canvas.drawOval(bound, paint); + resetStyle(style); + canvas.drawOval(bound, style.paint); } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); - canvas.drawOval(bound, paint); + resetStyle(style); + parseStyle(stream, skin, style); + canvas.drawOval(bound, style.paint); } } break; @@ -331,13 +360,13 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski int size = stream.readUnsignedByte(); if (size == 0) { - resetPaint(paint); - canvas.drawOval(bound, paint); + resetStyle(style); + canvas.drawOval(bound, style.paint); } else { for (int i = 0; i < size; ++i) { - resetPaint(paint); - parsePaint(stream, skinType, paint); - canvas.drawOval(bound, paint); + resetStyle(style); + parseStyle(stream, skin, style); + canvas.drawOval(bound, style.paint); } } break; @@ -361,6 +390,29 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski case COMMAND_PICTURE_DRAW_GROUP_END: canvas.restore(); break; + case COMMAND_PICTURE_DRAW_TEXT: { + float x = readCompressedFloat(stream); + float y = readCompressedFloat(stream); + short stringSize = stream.readShort(); + byte[] stringBuffer = new byte[stringSize]; + stream.read(stringBuffer); + String string = new String(stringBuffer, Charsets.UTF_8); + int size = stream.readByte(); + if (size == 0) { + resetStyle(style); + canvas.drawText(string, x, y, style.paint); + } else { + for (int i = 0; i < size; ++i) { + resetStyle(style); + parseStyle(stream, skin, style); + float drawY = style.dominantBaseline == COMMAND_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO + ? y + : y - (style.paint.ascent() + style.paint.descent()) / 2; + canvas.drawText(string, x, drawY, style.paint); + } + } + break; + } default: MozcLog.e("unknown command " + command); } @@ -370,20 +422,42 @@ private static PictureDrawable createPictureDrawable(DataInputStream stream, Ski return new MozcPictureDrawable(picture); } - private static void resetPaint(Paint paint) { - paint.reset(); - paint.setAntiAlias(true); + private void ensureTypeface(AssetManager assetManager) { + if (!typeface.isPresent()) { + synchronized (typeface) { + if (!typeface.isPresent()) { + try { + typeface = Optional.of(Typeface.createFromAsset(assetManager, FONT_PATH)); + } catch (RuntimeException e) { + // Typeface cannot be made. Use default typeface as fallback. + MozcLog.e(FONT_PATH + " is not accessible. Use system font."); + typeface = Optional.of(Typeface.DEFAULT); + } + } + } + } + } + + private static void resetStyle(MozcStyle style) { + style.paint.reset(); + style.paint.setAntiAlias(true); + style.paint.setTypeface(typeface.get()); + if (TEXT_LOCALE.isPresent()) { + style.paint.setTextLocale(TEXT_LOCALE.get()); + } + style.dominantBaseline = COMMAND_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO; } - private static void parsePaint(DataInputStream stream, SkinType skinType, Paint paint) + private static void parseStyle(DataInputStream stream, Skin skin, MozcStyle style) throws IOException { + Paint paint = style.paint; while (true) { int tag = stream.readByte() & 0xFF; if (tag >= 128) { // This is a bit tricky format, but the highest 1-bit means that the style should be // based on skin configuration. Delegate the paint to the skin. - skinType.apply(paint, tag & 0x7F); - return; + skin.apply(paint, tag & 0x7F); + continue; } switch (tag) { @@ -421,6 +495,35 @@ private static void parsePaint(DataInputStream stream, SkinType skinType, Paint paint.setShader(createShader(stream).orNull()); break; } + case COMMAND_PICTURE_PAINT_FONT_SIZE: { + paint.setTextSize(readCompressedFloat(stream)); + break; + } + case COMMAND_PICTURE_PAINT_TEXT_ANCHOR: { + byte value = stream.readByte(); + switch (value) { + case COMMAND_PICTURE_PAINT_TEXT_ANCHOR_START: + paint.setTextAlign(Align.LEFT); + break; + case COMMAND_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE: + paint.setTextAlign(Align.CENTER); + break; + case COMMAND_PICTURE_PAINT_TEXT_ANCHOR_END: + paint.setTextAlign(Align.RIGHT); + break; + default: + MozcLog.e("Unknown text-anchor : " + value, new Exception()); + } + break; + } + case COMMAND_PICTURE_PAINT_DOMINANT_BASELINE: { + style.dominantBaseline = stream.readByte(); + break; + } + case COMMAND_PICTURE_PAINT_FONT_WEIGHT: { + style.paint.setFakeBoldText(stream.readByte() == COMMAND_PICTURE_PAINT_FONT_WEIGHT_BOLD); + break; + } default: MozcLog.e("Unknown paint tag: " + tag, new Exception()); } @@ -584,12 +687,12 @@ private static Path createPath(DataInputStream stream) throws IOException { } private static StateListDrawable createStateListDrawable( - DataInputStream stream, SkinType skinType) throws IOException { + DataInputStream stream, Skin skin) throws IOException { int length = stream.readUnsignedByte(); StateListDrawable result = new StateListDrawable(); for (int i = 0; i < length; ++i) { int[] stateList = createStateList(stream); - Optional drawable = createDrawable(stream, skinType); + Optional drawable = createDrawable(stream, skin); result.addState(stateList, drawable.orNull()); } return result; diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageButton.java b/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageButton.java new file mode 100644 index 000000000..29062809d --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageButton.java @@ -0,0 +1,94 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.view; + +import com.google.common.base.Preconditions; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageButton; +import android.widget.ImageView; + +/** + * A ImageButton which accepts mozc drawable as src. + *

+ * See {@code MozcImageView}. + */ +public class MozcImageButton extends ImageButton implements MozcImageCapableView { + + private final MozcImageCapableViewDelegate delegate = new MozcImageCapableViewDelegate(this); + + public MozcImageButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Preconditions.checkArgument( + MozcImageCapableViewDelegate.assertSrcAttribute(getResources(), attrs)); + delegate.loadMozcImageSource(attrs); + } + + public MozcImageButton(Context context, AttributeSet attrs) { + super(context, attrs); + Preconditions.checkArgument( + MozcImageCapableViewDelegate.assertSrcAttribute(getResources(), attrs)); + delegate.loadMozcImageSource(attrs); + } + + public MozcImageButton(Context context) { + super(context); + } + + public void setRawId(int rawId) { + delegate.setRawId(rawId); + } + + public void setSkin(Skin skin) { + delegate.setSkin(skin); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + delegate.updateAdditionalPadding(); + } + + @Override + public void setMaxImageWidth(int maxImageWidth) { + delegate.setMaxImageWidth(maxImageWidth); + } + + @Override + public void setMaxImageHeight(int maxImageHeight) { + delegate.setMaxImageHeight(maxImageHeight); + } + + @Override + public ImageView asImageView() { + return this; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageCapableView.java b/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageCapableView.java new file mode 100644 index 000000000..bf84af607 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageCapableView.java @@ -0,0 +1,174 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.view; + +import org.mozc.android.inputmethod.japanese.resources.R; +import com.google.common.base.Preconditions; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +/** + * Views which support MozcDrawable should implement this interface + * delegate some methods. + */ +public interface MozcImageCapableView { + + public void setMaxImageWidth(int maxImageWidth); + public void setMaxImageHeight(int maxImageHeight); + public ImageView asImageView(); + + /** + * Delagate object used by the views implementing MozcImageCapableView. + */ + class MozcImageCapableViewDelegate { + + private static final int INVALID_RESOURCE_ID = 0; + private static final int UNSPECIFIED_SIZE = -1; + private static final String XML_NAMESPACE_ANDROID = + "http://schemas.android.com/apk/res/android"; + private static final String XML_NAMESPACE_MOZC = + "http://schemas.android.com/apk/res-auto"; + private Skin skin = Skin.getFallbackInstance(); + private int rawId = INVALID_RESOURCE_ID; + private int maxImageWidth = UNSPECIFIED_SIZE; + private int maxImageHeight = UNSPECIFIED_SIZE; + private final MozcImageCapableView baseView; + + MozcImageCapableViewDelegate(MozcImageCapableView baseView) { + this.baseView = Preconditions.checkNotNull(baseView); + // Disable h/w acceleration to use a PictureDrawable. + baseView.asImageView().setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + static boolean assertSrcAttribute(Resources resources, AttributeSet attrs) { + int srcId = attrs.getAttributeResourceValue( + XML_NAMESPACE_ANDROID, "src", INVALID_RESOURCE_ID); + if (srcId == INVALID_RESOURCE_ID) { + return true; + } + return !"raw".equals(resources.getResourceTypeName(srcId)); + } + + void loadMozcImageSource(AttributeSet attrs) { + rawId = attrs.getAttributeResourceValue( + XML_NAMESPACE_MOZC, "rawSrc", INVALID_RESOURCE_ID); + TypedArray typedArray = + baseView.asImageView().getContext().obtainStyledAttributes(attrs, + R.styleable.MozcImageView); + try { + maxImageWidth = + typedArray.getDimensionPixelOffset(R.styleable.MozcImageView_maxImageWidth, + UNSPECIFIED_SIZE); + maxImageHeight = + typedArray.getDimensionPixelOffset(R.styleable.MozcImageView_maxImageHeight, + UNSPECIFIED_SIZE); + } finally { + typedArray.recycle(); + } + } + + private void updateMozcDrawable() { + if (rawId != INVALID_RESOURCE_ID) { + ImageView view = baseView.asImageView(); + view.setImageDrawable( + skin.getDrawable(baseView.asImageView().getContext().getResources(), rawId)); + view.invalidate(); + } + } + + void setRawId(int rawId) { + this.rawId = rawId; + updateMozcDrawable(); + } + + @SuppressWarnings("deprecation") + void setSkin(Skin skin) { + this.skin = Preconditions.checkNotNull(skin); + updateMozcDrawable(); + } + + void updateAdditionalPadding() { + updateAdditionalHorizontalPadding(); + updateAdditionalVerticalPadding(); + } + + void setMaxImageWidth(int maxImageWidth) { + this.maxImageWidth = maxImageWidth; + updateAdditionalHorizontalPadding(); + } + + void setMaxImageHeight(int maxImageHeight) { + this.maxImageHeight = maxImageHeight; + updateAdditionalVerticalPadding(); + } + + private void updateAdditionalHorizontalPadding() { + ImageView imageView = baseView.asImageView(); + int paddingLeft = baseView.asImageView().getPaddingLeft(); + int paddingRight = baseView.asImageView().getPaddingRight(); + int additionalHorizontalPadding = maxImageWidth < 0 ? 0 : Math.max( + 0, baseView.asImageView().getWidth() - paddingLeft - paddingRight - maxImageWidth); + if (additionalHorizontalPadding == 0) { + return; + } + int additionalLeftPadding = additionalHorizontalPadding / 2; + int additionalRightPadding = additionalHorizontalPadding - additionalLeftPadding; + baseView.asImageView().setPadding( + paddingLeft + additionalLeftPadding, + imageView.getPaddingTop(), + paddingRight + additionalRightPadding, + imageView.getPaddingBottom()); + imageView.invalidate(); + } + + private void updateAdditionalVerticalPadding() { + ImageView imageView = baseView.asImageView(); + int paddingTop = imageView.getPaddingTop(); + int paddingBottom = imageView.getPaddingBottom(); + int additionalVerticalPadding = maxImageHeight < 0 + ? 0 : imageView.getHeight() - paddingTop - paddingBottom - maxImageHeight; + if (additionalVerticalPadding == 0) { + return; + } + int additionalTopPadding = additionalVerticalPadding / 2; + int additionalBottomPadding = additionalVerticalPadding - additionalTopPadding; + baseView.asImageView().setPadding( + imageView.getPaddingLeft(), + paddingTop + additionalTopPadding, + imageView.getPaddingRight(), + paddingBottom + additionalBottomPadding); + imageView.invalidate(); + } + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageView.java b/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageView.java new file mode 100644 index 000000000..5e08e9315 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/view/MozcImageView.java @@ -0,0 +1,104 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.view; + +import com.google.common.base.Preconditions; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * A ImageView which accepts mozc drawable as src. + *

+ * In addition to ImageView, following XML attributes are supported: + *

    + *
  • maxImageWidth(dimension): If set, additional padding (left and right) are automatically + * set in order to make drawn image thinner than given width. + * {@code setPadding} and {@code setPaddingRelative} are disabled + * (UnsupportedOperationException will be thrown}. + *
  • maxImageHeight(dimension): Same as maxImageWidth. + *
+ *

+ * This class is not intended to be changed its padding. + * There was a warning code for it but on old Android framework setPadding() is always called + * so the warning code has been removed. + */ +public class MozcImageView extends ImageView implements MozcImageCapableView { + + private final MozcImageCapableViewDelegate delegate = new MozcImageCapableViewDelegate(this); + + public MozcImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Preconditions.checkArgument( + MozcImageCapableViewDelegate.assertSrcAttribute(getResources(), attrs)); + delegate.loadMozcImageSource(attrs); + } + + public MozcImageView(Context context, AttributeSet attrs) { + super(context, attrs); + Preconditions.checkArgument( + MozcImageCapableViewDelegate.assertSrcAttribute(getResources(), attrs)); + delegate.loadMozcImageSource(attrs); + } + + public MozcImageView(Context context) { + super(context); + } + + public void setRawId(int rawId) { + delegate.setRawId(rawId); + } + + public void setSkin(Skin skin) { + delegate.setSkin(skin); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + delegate.updateAdditionalPadding(); + } + + @Override + public void setMaxImageWidth(int maxImageWidth) { + delegate.setMaxImageWidth(maxImageWidth); + } + + @Override + public void setMaxImageHeight(int maxImageHeight) { + delegate.setMaxImageHeight(maxImageHeight); + } + + @Override + public ImageView asImageView() { + return this; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/MozcPictureDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/MozcPictureDrawable.java index f7562968b..fc53a4311 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/MozcPictureDrawable.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/MozcPictureDrawable.java @@ -34,6 +34,7 @@ import android.graphics.Canvas; import android.graphics.Picture; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.graphics.drawable.PictureDrawable; /** @@ -48,6 +49,9 @@ public MozcPictureDrawable(Picture picture) { @Override public void draw(Canvas canvas) { + Preconditions.checkArgument(!canvas.isHardwareAccelerated(), + "MozcPictureDrawable doesn't accept h/w accelerated canvas, " + + "which doesn't support drawPicture()."); Picture picture = getPicture(); if (picture == null) { return; @@ -65,4 +69,19 @@ public void draw(Canvas canvas) { canvas.restoreToCount(saveCount); } } + + @Override + public ConstantState getConstantState() { + return new ConstantState() { + @Override + public int getChangingConfigurations() { + return 0; + } + + @Override + public Drawable newDrawable() { + return new MozcPictureDrawable(getPicture()); + } + }; + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/PopUpFrameWindowDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/PopUpFrameWindowDrawable.java deleted file mode 100644 index c2cccd20e..000000000 --- a/src/android/src/com/google/android/inputmethod/japanese/view/PopUpFrameWindowDrawable.java +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2010-2014, Google Inc. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package org.mozc.android.inputmethod.japanese.view; - -import com.google.common.base.Optional; - -import android.graphics.BlurMaskFilter; -import android.graphics.BlurMaskFilter.Blur; -import android.graphics.Canvas; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.graphics.Rect; -import android.graphics.Shader; -import android.graphics.Shader.TileMode; - -/** - * Drawable to render a popup window with a frame. - * - */ -public class PopUpFrameWindowDrawable extends BaseBackgroundDrawable { - - private final int frameWidth; - - private final int topColor; - private final int bottomColor; - private final int borderColor; - private final int innerPaneColor; - private final float shadowSize; - - private final Rect outerFrameRect = new Rect(); - private final Rect innerFrameRect = new Rect(); - private final Paint paint = new Paint(); - private final BlurMaskFilter blurMaskFilter; - private Optional shader = Optional.absent(); - - public PopUpFrameWindowDrawable( - int leftPadding, int topPadding, int rightPadding, int bottomPadding, - int frameWidth, float shadowSize, - int topColor, int bottomColor, int borderColor, int innerPaneColor) { - super(leftPadding, topPadding, rightPadding, bottomPadding); - - this.frameWidth = frameWidth; - this.topColor = topColor; - this.bottomColor = bottomColor; - this.borderColor = borderColor; - this.innerPaneColor = innerPaneColor; - this.shadowSize = shadowSize; - this.blurMaskFilter = new BlurMaskFilter(shadowSize, Blur.INNER); - } - - @Override - public void draw(Canvas canvas) { - if (isCanvasRectEmpty()) { - return; - } - - Paint paint = this.paint; - - // First of all, render the shadow. - paint.reset(); - paint.setAntiAlias(true); - paint.setColor(0x80000000); - paint.setShadowLayer(shadowSize * 0.6f, shadowSize, shadowSize, 0xFF404040); - canvas.drawRect(outerFrameRect, paint); - - // Draw the main part of the frame. - if (shader.isPresent()) { - paint.reset(); - paint.setAntiAlias(true); - paint.setShader(shader.get()); - canvas.drawRect(outerFrameRect, paint); - } - - // Draw the inner pane. - paint.reset(); - paint.setAntiAlias(true); - paint.setColor(0xFF000000); - canvas.drawRect(innerFrameRect, paint); - - paint.setColor(innerPaneColor); - paint.setMaskFilter(blurMaskFilter); - canvas.drawRect(innerFrameRect, paint); - - // Draw the border. - paint.reset(); - paint.setAntiAlias(true); - paint.setColor(borderColor); - paint.setStyle(Style.STROKE); - - canvas.drawRect(outerFrameRect, paint); - canvas.drawRect(innerFrameRect, paint); - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - - Rect canvasRect = getCanvasRect(); - - outerFrameRect.set(canvasRect); - innerFrameRect.set( - canvasRect.left + frameWidth, canvasRect.top + frameWidth, - canvasRect.right - frameWidth, canvasRect.bottom - frameWidth); - shader = Optional.of(new LinearGradient( - 0, canvasRect.top, 0, canvasRect.bottom, topColor, bottomColor, TileMode.CLAMP)); - } -} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/VerticalInnerDropShadowDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/QwertySpaceKeyDrawable.java similarity index 50% rename from src/android/src/com/google/android/inputmethod/japanese/view/VerticalInnerDropShadowDrawable.java rename to src/android/src/com/google/android/inputmethod/japanese/view/QwertySpaceKeyDrawable.java index ac626c7f2..66012f0b2 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/VerticalInnerDropShadowDrawable.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/QwertySpaceKeyDrawable.java @@ -29,64 +29,29 @@ package org.mozc.android.inputmethod.japanese.view; -import com.google.common.base.Optional; - -import android.graphics.Canvas; -import android.graphics.LinearGradient; -import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.Shader.TileMode; - -/** - * Drawable for drop shadow on top and bottom of the boundary. - */ -public class VerticalInnerDropShadowDrawable extends BaseBackgroundDrawable { - - private final int shadowSize; - private Optional topShadow = Optional.absent(); - private Optional bottomShadow = Optional.absent(); - private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - public VerticalInnerDropShadowDrawable( - int leftPadding, int topPadding, int rightPadding, int bottomPadding, - int shadowSize) { - super(leftPadding, topPadding, rightPadding, bottomPadding); - this.shadowSize = shadowSize; - } - - @Override - public void draw(Canvas canvas) { - if (isCanvasRectEmpty()) { - return; - } - - Rect rect = getCanvasRect(); - if (topShadow.isPresent()) { - paint.setShader(topShadow.get()); - canvas.drawRect(rect, paint); - } - if (bottomShadow.isPresent()) { - paint.setShader(bottomShadow.get()); - canvas.drawRect(rect, paint); - } +/** The space key implementation for qwerty keyboards. */ +public class QwertySpaceKeyDrawable extends RoundRectKeyDrawable { + private static final int UNLIMITED_HEIGHT = 0; + private final int height; + + public QwertySpaceKeyDrawable( + int height, int leftPadding, int topPadding, int rightPadding, int bottomPadding, + int roundSize, int topColor, int bottomColor, int highlightColor, int shadowColor) { + super(leftPadding, topPadding, rightPadding, bottomPadding, + roundSize, topColor, bottomColor, highlightColor, shadowColor); + this.height = height; } @Override protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - if (isCanvasRectEmpty()) { - topShadow = Optional.absent(); - bottomShadow = Optional.absent(); - return; + if (height != UNLIMITED_HEIGHT && bounds.height() > height) { + int topPadding = (bounds.height() - height) / 2; + int bottomPadding = (bounds.height() - height) - topPadding; + bounds = new Rect(bounds.left, bounds.top + topPadding, + bounds.right, bounds.bottom - bottomPadding); } - - Rect canvasRect = getCanvasRect(); - topShadow = Optional.of(new LinearGradient( - 0, canvasRect.top, 0, Math.min(canvasRect.bottom, canvasRect.top + shadowSize), - 0xA6000000, 0x00000000, TileMode.CLAMP)); - bottomShadow = Optional.of(new LinearGradient( - 0, Math.max(canvasRect.top, canvasRect.bottom - shadowSize), 0, canvasRect.bottom, - 0x00000000, 0xA6000000, TileMode.CLAMP)); + super.onBoundsChange(bounds); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/RoundRectKeyDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/RoundRectKeyDrawable.java index 49fe76c4c..81c4cabd6 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/RoundRectKeyDrawable.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/RoundRectKeyDrawable.java @@ -34,6 +34,7 @@ import android.graphics.BlurMaskFilter; import android.graphics.BlurMaskFilter.Blur; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Style; @@ -55,7 +56,7 @@ public class RoundRectKeyDrawable extends BaseBackgroundDrawable { private final int bottomColor; private final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Optional shadowPaint; private final Optional highlightPaint; private final RectF baseBound = new RectF(); @@ -69,10 +70,15 @@ public RoundRectKeyDrawable( this.topColor = topColor; this.bottomColor = bottomColor; - shadowPaint.setColor(shadowColor); - shadowPaint.setStyle(Style.FILL); - shadowPaint.setMaskFilter(new BlurMaskFilter(BLUR_SIZE, Blur.NORMAL)); - if ((highlightColor & 0xFF000000) != 0) { + if (Color.alpha(shadowColor) != 0) { + shadowPaint = Optional.of(new Paint(Paint.ANTI_ALIAS_FLAG)); + shadowPaint.get().setColor(shadowColor); + shadowPaint.get().setStyle(Style.FILL); + shadowPaint.get().setMaskFilter(new BlurMaskFilter(BLUR_SIZE, Blur.NORMAL)); + } else { + shadowPaint = Optional.absent(); + } + if (Color.alpha(highlightColor) != 0) { highlightPaint = Optional.of(new Paint(Paint.ANTI_ALIAS_FLAG)); highlightPaint.get().setColor(highlightColor); } else { @@ -87,7 +93,9 @@ public void draw(Canvas canvas) { } // Each qwerty key looks round corner'ed rectangle. - canvas.drawRoundRect(shadowBound, roundSize, roundSize, shadowPaint); + if (shadowPaint.isPresent()) { + canvas.drawRoundRect(shadowBound, roundSize, roundSize, shadowPaint.get()); + } canvas.drawRoundRect(baseBound, roundSize, roundSize, basePaint); // Draw 1-px height highlight at the top of key if necessary. diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/Skin.java b/src/android/src/com/google/android/inputmethod/japanese/view/Skin.java new file mode 100644 index 000000000..1e8fc74f1 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/view/Skin.java @@ -0,0 +1,336 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.view; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; + +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; + + +/** + * Skin data. + */ +public class Skin { + + private static final Skin FALLBACK_INSTANCE = new Skin(); + + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_MAIN = 0; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_GUIDE = 1; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_GUIDE_LIGHT = 2; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT = 3; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT = 4; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_BOUND = 5; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_TWELVEKEYS_FUNCTION = 6; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_TWELVEKEYS_GLOBE = 7; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_QWERTY_FUNCTION = 8; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_FUNCTION_DARK = 9; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_ENTER = 10; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_ENTER_CIRCLE = 11; + @VisibleForTesting static final int STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW = 12; + @VisibleForTesting static final int STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT = 13; + @VisibleForTesting static final int STYLE_CATEGORY_SYMBOL_MAJOR = 14; + @VisibleForTesting static final int STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED = 15; + @VisibleForTesting static final int STYLE_CATEGORY_SYMBOL_MAJOR_EMOJI_DISABLE_CIRCLE = 16; + @VisibleForTesting static final int STYLE_CATEGORY_SYMBOL_MINOR = 17; + @VisibleForTesting static final int STYLE_CATEGORY_SYMBOL_MINOR_SELECTED = 18; + @VisibleForTesting + static final int STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_DEFAULT = 19; + @VisibleForTesting + static final int STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_SCROLLED = 20; + + public int keyboardSeparatorColor; + public int twelvekeysLayoutReleasedKeyTopColor; + public int twelvekeysLayoutReleasedKeyBottomColor; + public int twelvekeysLayoutReleasedKeyHighlightColor; + public int twelvekeysLayoutReleasedKeyLightShadeColor; + public int twelvekeysLayoutReleasedKeyDarkShadeColor; + public int twelvekeysLayoutReleasedKeyShadowColor; + + public int twelvekeysLayoutPressedKeyTopColor; + public int twelvekeysLayoutPressedKeyBottomColor; + public int twelvekeysLayoutPressedKeyHighlightColor; + public int twelvekeysLayoutPressedKeyLightShadeColor; + public int twelvekeysLayoutPressedKeyDarkShadeColor; + public int twelvekeysLayoutPressedKeyShadowColor; + + public int twelvekeysLayoutReleasedFunctionKeyTopColor; + public int twelvekeysLayoutReleasedFunctionKeyBottomColor; + public int twelvekeysLayoutReleasedFunctionKeyHighlightColor; + public int twelvekeysLayoutReleasedFunctionKeyLightShadeColor; + public int twelvekeysLayoutReleasedFunctionKeyDarkShadeColor; + public int twelvekeysLayoutReleasedFunctionKeyShadowColor; + + public int twelvekeysLayoutPressedFunctionKeyTopColor; + public int twelvekeysLayoutPressedFunctionKeyBottomColor; + public int twelvekeysLayoutPressedFunctionKeyHighlightColor; + public int twelvekeysLayoutPressedFunctionKeyLightShadeColor; + public int twelvekeysLayoutPressedFunctionKeyDarkShadeColor; + public int twelvekeysLayoutPressedFunctionKeyShadowColor; + + public int qwertyLayoutReleasedKeyTopColor; + public int qwertyLayoutReleasedKeyBottomColor; + public int qwertyLayoutReleasedKeyHighlightColor; + public int qwertyLayoutReleasedKeyShadowColor; + + public int qwertyLayoutPressedKeyTopColor; + public int qwertyLayoutPressedKeyBottomColor; + public int qwertyLayoutPressedKeyHighlightColor; + public int qwertyLayoutPressedKeyShadowColor; + + public int qwertyLayoutReleasedFunctionKeyTopColor; + public int qwertyLayoutReleasedFunctionKeyBottomColor; + public int qwertyLayoutReleasedFunctionKeyHighlightColor; + public int qwertyLayoutReleasedFunctionKeyShadowColor; + + public int qwertyLayoutPressedFunctionKeyTopColor; + public int qwertyLayoutPressedFunctionKeyBottomColor; + public int qwertyLayoutPressedFunctionKeyHighlightColor; + public int qwertyLayoutPressedFunctionKeyShadowColor; + + public int qwertyLayoutReleasedSpaceKeyTopColor; + public int qwertyLayoutReleasedSpaceKeyBottomColor; + public int qwertyLayoutReleasedSpaceKeyHighlightColor; + public int qwertyLayoutReleasedSpaceKeyShadowColor; + + public int qwertyLayoutPressedSpaceKeyTopColor; + public int qwertyLayoutPressedSpaceKeyBottomColor; + public int qwertyLayoutPressedSpaceKeyHighlightColor; + public int qwertyLayoutPressedSpaceKeyShadowColor; + + public int flickBaseColor; + public int flickShadeColor; + + public int popupFrameWindowTopColor; + public int popupFrameWindowBottomColor; + public int popupFrameWindowShadowColor; + + public int candidateScrollBarTopColor; + public int candidateScrollBarBottomColor; + public int candidateBackgroundTopColor; + public int candidateBackgroundBottomColor; + public int candidateBackgroundSeparatorColor; + public int candidateBackgroundHighlightColor; + public int candidateBackgroundBorderColor; + public int candidateBackgroundFocusedTopColor; + public int candidateBackgroundFocusedBottomColor; + public int candidateBackgroundFocusedShadowColor; + + public int symbolReleasedFunctionKeyTopColor; + public int symbolReleasedFunctionKeyBottomColor; + public int symbolReleasedFunctionKeyHighlightColor; + public int symbolReleasedFunctionKeyShadowColor; + + public int symbolPressedFunctionKeyTopColor; + public int symbolPressedFunctionKeyBottomColor; + public int symbolPressedFunctionKeyHighlightColor; + public int symbolPressedFunctionKeyShadowColor; + + public int symbolScrollBarTopColor; + public int symbolScrollBarBottomColor; + + public int symbolMinorCategoryTabSelectedColor; + public int symbolMinorCategoryTabPressedColor; + + public int symbolCandidateBackgroundTopColor; + public int symbolCandidateBackgroundBottomColor; + public int symbolCandidateBackgroundHighlightColor; + public int symbolCandidateBackgroundBorderColor; + public int symbolCandidateBackgroundSeparatorColor; + public int symbolMinorCategoryVerticalSeparatorColor; + public int symbolSeparatorColor; + + public int threeDotsColor; + + public int keyIconMainColor; + public int keyIconGuideColor; + public int keyIconGuideLightColor; + public int keyIconMainHighlightColor; + public int keyIconGuideHighlightColor; + public int keyIconBoundColor; + public int keyIconTwelvekeysFunctionColor; + public int keyIconTwelvekeysGlobeColor; + public int keyIconQwertyFunctionColor; + public int keyIconFunctionDarkColor; + public int keyIconEnterColor; + public int keyIconEnterCircleColor; + public int keyIconQwertyShiftOnArrowColor; + public int keyPopupHighlightColor; + public int symbolMajorColor; + public int symbolMajorSelectedColor; + public int symbolMinorColor; + public int symbolMinorSelectedColor; + public int symbolMajorButtonTopColor; + public int symbolMajorButtonBottomColor; + public int symbolMajorButtonPressedTopColor; + public int symbolMajorButtonPressedBottomColor; + public int symbolMajorButtonSelectedTopColor; + public int symbolMajorButtonSelectedBottomColor; + public int symbolMajorButtonShadowColor; + public int keyboardFoldingButtonBackgroundDefaultColor; + public int keyboardFoldingButtonBackgroundScrolledColor; + public int candidateValueTextColor = Color.BLACK; + public int candidateValueFocusedTextColor = Color.BLACK; + public int candidateDescriptionTextColor = Color.GRAY; + public int buttonFrameButtonPressedColor; + + public float twelvekeysLeftOffsetDimension; + public float twelvekeysRightOffsetDimension; + public float twelvekeysTopOffsetDimension; + public float twelvekeysBottomOffsetDimension; + public float qwertyLeftOffsetDimension; + public float qwertyRightOffsetDimension; + public float qwertyTopOffsetDimension; + public float qwertyBottomOffsetDimension; + public float qwertyRoundRadiusDimension; + public float qwertySpaceKeyHeightDimension; + public float qwertySpaceKeyHorizontalOffsetDimension; + public float qwertySpaceKeyRoundRadiusDimension; + public float symbolMajorButtonRoundDimension; + public float symbolMajorButtonPaddingDimension; + public float symbolMinorIndicatorHeightDimension; + + public Drawable buttonFrameBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable narrowFrameBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable keyboardFrameSeparatorBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable symbolSeparatorAboveMajorCategoryBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable windowBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable conversionCandidateViewBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable symbolCandidateViewBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable symbolMajorCategoryBackgroundDrawable = DummyDrawable.getInstance(); + public Drawable scrollBarBackgroundDrawable = DummyDrawable.getInstance(); + + private Optional drawableFactory = Optional.absent(); + + public static Skin getFallbackInstance() { + return FALLBACK_INSTANCE; + } + + public void apply(Paint paint, int category) { + Preconditions.checkNotNull(paint); + + // At the moment, caller has responsibility to reset paint. + // TODO(matsuzakit): move all style based logic from MozcDrawableFactory to here. + paint.setStyle(Style.FILL); + switch (category) { + case STYLE_CATEGORY_KEYICON_MAIN: + paint.setColor(keyIconMainColor); + break; + case STYLE_CATEGORY_KEYICON_GUIDE: + paint.setColor(keyIconGuideColor); + break; + case STYLE_CATEGORY_KEYICON_GUIDE_LIGHT: + paint.setColor(keyIconGuideLightColor); + break; + case STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT: + paint.setColor(keyIconMainHighlightColor); + break; + case STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT: + paint.setColor(keyIconGuideHighlightColor); + break; + case STYLE_CATEGORY_KEYICON_BOUND: + paint.setColor(keyIconBoundColor); + paint.setStyle(Style.STROKE); + break; + case STYLE_CATEGORY_KEYICON_TWELVEKEYS_FUNCTION: + paint.setColor(keyIconTwelvekeysFunctionColor); + break; + case STYLE_CATEGORY_KEYICON_TWELVEKEYS_GLOBE: + paint.setColor(keyIconTwelvekeysGlobeColor); + break; + case STYLE_CATEGORY_KEYICON_QWERTY_FUNCTION: + paint.setColor(keyIconQwertyFunctionColor); + break; + case STYLE_CATEGORY_KEYICON_FUNCTION_DARK: + paint.setColor(keyIconFunctionDarkColor); + break; + case STYLE_CATEGORY_KEYICON_ENTER: + paint.setColor(keyIconEnterColor); + break; + case STYLE_CATEGORY_KEYICON_ENTER_CIRCLE: + paint.setColor(keyIconEnterCircleColor); + break; + case STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW: + paint.setColor(keyIconQwertyShiftOnArrowColor); + break; + case STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT: + paint.setColor(keyPopupHighlightColor); + break; + case STYLE_CATEGORY_SYMBOL_MAJOR: + paint.setColor(symbolMajorColor); + break; + case STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED: + paint.setColor(symbolMajorSelectedColor); + break; + case STYLE_CATEGORY_SYMBOL_MAJOR_EMOJI_DISABLE_CIRCLE: + paint.setStyle(Style.STROKE); + paint.setColor(symbolMajorColor); + paint.setStrokeWidth(0.75f); + paint.setStrokeMiter(10); + break; + case STYLE_CATEGORY_SYMBOL_MINOR: + paint.setColor(symbolMinorColor); + break; + case STYLE_CATEGORY_SYMBOL_MINOR_SELECTED: + paint.setColor(symbolMinorSelectedColor); + break; + case STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_DEFAULT: + paint.setColor(keyboardFoldingButtonBackgroundDefaultColor); + break; + case STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_SCROLLED: + paint.setColor(keyboardFoldingButtonBackgroundScrolledColor); + break; + default: + throw new IllegalStateException("Unknown category: " + category); + } + } + + public Drawable getDrawable(Resources resources, int resourceId) { + Preconditions.checkNotNull(resources); + return getDrawableFactory(resources).getDrawable(resourceId).or(DummyDrawable.getInstance()); + } + + private MozcDrawableFactory getDrawableFactory(Resources resources) { + if (!drawableFactory.isPresent()) { + drawableFactory = Optional.of(new MozcDrawableFactory(resources, this)); + } + return drawableFactory.get(); + } + + @VisibleForTesting + void resetDrawableFactory() { + drawableFactory = Optional.absent(); + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/SkinParser.java b/src/android/src/com/google/android/inputmethod/japanese/view/SkinParser.java new file mode 100644 index 000000000..4758e4dd4 --- /dev/null +++ b/src/android/src/com/google/android/inputmethod/japanese/view/SkinParser.java @@ -0,0 +1,229 @@ +// Copyright 2010-2014, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package org.mozc.android.inputmethod.japanese.view; + +import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.util.ParserUtil; +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +/** + * Parses .xml file for skin and generates {@code Skin} instance. + */ +public class SkinParser { + + static class SkinParserException extends Exception { + + public SkinParserException(XmlPullParser parser, Throwable throwable) { + super(composeMessage(parser, throwable.getLocalizedMessage()), throwable); + } + + public SkinParserException(XmlPullParser parser, String detailMessage) { + super(composeMessage(parser, detailMessage)); + } + + private static String composeMessage(XmlPullParser parser, String message) { + return new StringBuffer(parser.getPositionDescription()) + .append(':').append(message).toString(); + } + } + + private final XmlResourceParser parser; + private final Resources resources; + + private static final int[] COLOR_ATTRIBUTES = { + android.R.attr.name, + android.R.attr.color, + }; + static { + Arrays.sort(COLOR_ATTRIBUTES); + } + private static final int COLOR_KEY_NAME_INDEX = + Arrays.binarySearch(COLOR_ATTRIBUTES, android.R.attr.name); + private static final int COLOR_KEY_COLOR_INDEX = + Arrays.binarySearch(COLOR_ATTRIBUTES, android.R.attr.color); + + private static final int[] DRAWABLE_ATTRIBUTES = { + android.R.attr.name, + }; + private static final int DRAWABLE_KEY_NAME_INDEX = + Arrays.binarySearch(DRAWABLE_ATTRIBUTES, android.R.attr.name); + + private static final int[] DIMENSION_ATTRIBUTES = { + android.R.attr.name, + R.attr.dimension, + }; + static { + Arrays.sort(DIMENSION_ATTRIBUTES); + } + private static final int DIMENSION_KEY_NAME_INDEX = + Arrays.binarySearch(DIMENSION_ATTRIBUTES, android.R.attr.name); + private static final int DIMENSION_KEY_DIMENSION_INDEX = + Arrays.binarySearch(DIMENSION_ATTRIBUTES, R.attr.dimension); + + public SkinParser(Resources resources, XmlResourceParser parser) { + this.resources = Preconditions.checkNotNull(resources); + this.parser = Preconditions.checkNotNull(parser); + } + + private static final Map fieldMap; + static { + Map tempMap = Maps.newHashMapWithExpectedSize(Skin.class.getFields().length); + for (Field field : Skin.class.getFields()) { + tempMap.put(field.getName(), field); + } + fieldMap = Collections.unmodifiableMap(tempMap); + } + + public Skin parseSkin() throws SkinParserException { + XmlResourceParser parser = this.parser; + Skin skin = new Skin(); + AttributeSet attributeSet = Xml.asAttributeSet(parser); + + try { + // Initial two events should be START_DOCUMENT and then START_TAG. + parser.next(); + ParserUtil.assertStartDocument(parser); + parser.nextTag(); + if (!"Skin".equals(parser.getName())) { + throw new SkinParserException(parser, + " element is expected but met <" + parser.getName() + ">"); + } + + while (parser.nextTag() == XmlResourceParser.START_TAG) { + String tagName = parser.getName(); + if ("Color".equals(tagName)) { + TypedArray attributes = resources.obtainAttributes(parser, COLOR_ATTRIBUTES); + try { + String name = attributes.getString(COLOR_KEY_NAME_INDEX); + if (name == null) { + throw new SkinParserException(parser, + " element's \"name\" attribute is mandatory."); + } + int color = attributes.getColor(COLOR_KEY_COLOR_INDEX, 0); + Field field = fieldMap.get(name); + if (field == null) { + throw new SkinParserException(parser, name + " is undefined field."); + } + field.setInt(skin, color); + if (parser.nextTag() != XmlResourceParser.END_TAG) { + throw new SkinParserException(parser, " is expected but not found."); + } + } finally { + attributes.recycle(); + } + continue; + } + if ("Drawable".equals(tagName)) { + TypedArray attributes = resources.obtainAttributes(parser, DRAWABLE_ATTRIBUTES); + try { + String name = attributes.getString(DRAWABLE_KEY_NAME_INDEX); + if (name == null) { + throw new SkinParserException(parser, + " element's \"name\" attribute is mandatory."); + } + // Go forward to inner drawable tag. + if (parser.nextTag() != XmlResourceParser.START_TAG) { + throw new SkinParserException(parser, "Start tag for drawable is expected."); + } + Drawable drawable = Drawable.createFromXmlInner(resources, parser, attributeSet); + if (drawable == null) { + throw new SkinParserException(parser, "Invalid drawable."); + } + if (parser.nextTag() != XmlResourceParser.END_TAG) { + throw new SkinParserException(parser, "End tag for drawable is expected."); + } + Field field = fieldMap.get(name); + if (field == null) { + throw new SkinParserException(parser, name + " is undefined field."); + } + field.set(skin, drawable); + if (parser.nextTag() != XmlResourceParser.END_TAG) { // Skip end tag. + throw new SkinParserException(parser, " is expected but not found."); + } + } finally { + attributes.recycle(); + } + continue; + } + if ("Dimension".equals(tagName)) { + TypedArray attributes = resources.obtainAttributes(parser, DIMENSION_ATTRIBUTES); + try { + String name = attributes.getString(DIMENSION_KEY_NAME_INDEX); + if (name == null) { + throw new SkinParserException(parser, + " element's \"name\" attribute is mandatory."); + } + float dimension = attributes.getDimension(DIMENSION_KEY_DIMENSION_INDEX, 0); + Field field = fieldMap.get(name); + if (field == null) { + throw new SkinParserException(parser, name + " is undefined field."); + } + field.setFloat(skin, dimension); + if (parser.nextTag() != XmlResourceParser.END_TAG) { + throw new SkinParserException(parser, " is expected but not found."); + } + } finally { + attributes.recycle(); + } + continue; + } + throw new SkinParserException(parser, "Unexpected <" + tagName + "> is found."); + } + parser.next(); + ParserUtil.assertEndDocument(parser); + } catch (IllegalAccessException e) { + throw new SkinParserException(parser, e); + } catch (IllegalArgumentException e) { + throw new SkinParserException(parser, e); + } catch (XmlPullParserException e) { + throw new SkinParserException(parser, e); + } catch (IOException e) { + throw new SkinParserException(parser, e); + } + return skin; + } +} diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/SkinType.java b/src/android/src/com/google/android/inputmethod/japanese/view/SkinType.java index 8919ea244..8f273077d 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/SkinType.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/SkinType.java @@ -29,765 +29,48 @@ package org.mozc.android.inputmethod.japanese.view; +import org.mozc.android.inputmethod.japanese.MozcLog; import org.mozc.android.inputmethod.japanese.resources.R; +import org.mozc.android.inputmethod.japanese.view.SkinParser.SkinParserException; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; -import android.graphics.Paint; -import android.graphics.Paint.Style; +import android.content.res.Resources; /** * Type of skins. */ public enum SkinType { - ORANGE_LIGHTGRAY( - // 12keys layout regular released key config. - 0xFFF5F5F5, 0xFFD2D2D2, 0xFFFAFAFA, 0xFFAFAFAF, 0xFF909090, 0xFF1E1E1E, - - // 12keys layout regular pressed key config. - 0xFFAAAAAA, 0xFF828282, 0, 0, 0, 0, - - // 12keys layout function released key config. - 0xFF858087, 0xFF67645F, 0xFF898588, 0xFF5C5759, 0xFF555555, 0xFF1E1E1E, - - // 12keys layout regular pressed key config. - 0xFFBFBFBD, 0xFFF7F5EC, 0, 0, 0, 0, - - // Qwerty layout regular released key config. - 0xFFF5F5F5, 0xFFD2D2D2, 0, 0xFF1E1E1E, - - // Qwerty layout regular pressed key config. - 0xFFCCCCCC, 0xFF797979, 0, 0xFF1E1E1E, - - // Qwerty layout function released key config. - 0xFF858087, 0xFF67645F, 0, 0xFF1E1E1E, - - // Qwerty layout function pressed key config. - 0xFFE9E4E4, 0xFFB2ADAD, 0, 0xFF1E1E1E, - - // Flick color config. - // According to the original design mock, the base color is orange for now. - // The shade is 25% alpha-black. The following value is offline-calculated to mix base color. - 0xFFFF9A28, 0xFFC0741e, - - // Qwerty light on/off sign config. - 0xFFFFCC00, 0xFFFF9900, 0xFFFF9966, 0xFF333333, 0xFF333333, 0xFF000000, - - // Qwerty round coner radius. - 3.5f, - - // Framed popup window config. - 0xFFFFB53A, 0xFFFF7A25, 0xFF793D00, 0xFFFFFFFF, 0, - - // Candidate scroll indicator config. - 0xFFFFE39D, 0xFFFFCC33, - - // Candidate background config. - 0xFFE8E8E8, 0xFFDCDCDC, 0xFFFFFFFF, 0xFF8C8C8C, - - // Candidate focused background config. - 0xFFFFC142, 0xFFFFe096, 0x40000000, - - // Symbol function released key config. - 0xFF858087, 0xFF67645F, 0, 0xFF1E1E1E, - - // Symbol function pressed key config. - 0xFFE9E4E4, 0xFFB2ADAD, 0, 0xFF1E1E1E, - - // Symbol scroll indicator config. - 0xFFFFCC00, 0xFFFF9C00, - - // Symbol minor category tab config. - 0xFFFF9200, 0x80FF9200, - - // Symbol candidate background config. - 0xFFFEFEFE, 0xFFECECEC, 0xFFFFFFFF, 0x7C666666, - - // Three dots - 0xFFDDDDDD, - - // Window background resource. - R.drawable.window__background) { - @Override - public void apply(Paint paint, int category) { - Preconditions.checkNotNull(paint); - - // At the moment, caller has responsibility to reset paint. - // TODO(hidehiko): move all style based logic from MozcDrawableFactory to here. - switch (category) { - case STYLE_CATEGORY_KEYICON_MAIN: - case STYLE_CATEGORY_KEYICON_FUNCTION_DARK: - case STYLE_CATEGORY_KEYICON_POPUP_FUNCTION: - paint.setStyle(Style.FILL); - paint.setColor(0xFF272727); - break; - case STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFFFFF); - paint.setShadowLayer(0.5f, 0.f, -1.f, 0xFF404040); - break; - case STYLE_CATEGORY_KEYICON_GUIDE: - case STYLE_CATEGORY_SYMBOL_MAJOR: - paint.setStyle(Style.FILL); - paint.setColor(0xFF333333); - break; - case STYLE_CATEGORY_KEYICON_GUIDE_LIGHT: - paint.setStyle(Style.FILL); - paint.setColor(0xFF999999); - break; - case STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT: - case STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT: - case STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED: - case STYLE_CATEGORY_SYMBOL_MINOR: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFFFFF); - break; - case STYLE_CATEGORY_KEYICON_BOUND: - paint.setStyle(Style.STROKE); - paint.setColor(0xFFCCCCCC); - break; - case STYLE_CATEGORY_KEYICON_FUNCTION: - paint.setStyle(Style.FILL); - paint.setColor(0xFFDDDDDD); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case SkinType.STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFB005); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case SkinType.STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW: - paint.setStyle(Style.FILL); - paint.setColor(0xFFC1F300); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT: - paint.setStyle(Style.FILL); - paint.setColor(0xFFF7982d); - break; - case STYLE_CATEGORY_SYMBOL_MINOR_SELECTED: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFF9a28); - break; - case STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND: - paint.setStyle(Style.FILL); - paint.setColor(0x8C676767); - break; - default: - throw new IllegalStateException("Unknown category: " + category); - } - } - }, - - BLUE_LIGHTGRAY( - // 12keys layout regular released key config. - 0xFFF5F5F5, 0xFFD2D2D2, 0xFFFAFAFA, 0xFFAFAFAF, 0xFF909090, 0xFF1E1E1E, - - // 12keys layout regular pressed key config. - 0xFFAAAAAA, 0xFF828282, 0, 0, 0, 0, - - // 12keys layout function released key config. - 0xFF858087, 0xFF67645F, 0xFF898588, 0xFF5C5759, 0xFF555555, 0xFF1E1E1E, - - // 12keys layout regular pressed key config. - 0xFFBFBFBD, 0xFFF7F5EC, 0, 0, 0, 0, - - // Qwerty layout regular released key config. - 0xFFF5F5F5, 0xFFD2D2D2, 0, 0xFF1E1E1E, - - // Qwerty layout regular pressed key config. - 0xFFCCCCCC, 0xFF797979, 0, 0xFF1E1E1E, - - // Qwerty layout function released key config. - 0xFF858087, 0xFF67645F, 0, 0xFF1E1E1E, - - // Qwerty layout function pressed key config. - 0xFFE9E4E4, 0xFFB2ADAD, 0, 0xFF1E1E1E, - - // Flick color config. - // According to the original design mock, the base color is orange for now. - // The shade is 25% alpha-black. The following value is offline-calculated to mix base color. - 0xFF55C6EE, 0xFF3F94B2, - - // Qwerty light on/off sign config. - 0xFF73DAF7, 0xFF3393E5, 0xFF53B6EE, 0xFF333333, 0xFF333333, 0xFF000000, - - // Qwerty round coner radius. - 3.5f, - - // Framed popup window config. - 0xFFFBFBFB, 0xFFEAEAEA, 0, 0, 0xFF1E1E1E, - - // Candidate scroll indicator config. - 0xFF73DAF7, 0xFF3393E5, - - // Candidate background config. - 0xFFE8E8E8, 0xFFDCDCDC, 0xFFFFFFFF, 0xFF8C8C8C, - - // Candidate focused background config. - 0xFFA5DDF6, 0xFFC1E9F5, 0x40000000, - - // Symbol function released key config. - 0xFF858087, 0xFF67645F, 0, 0xFF1E1E1E, - - // Symbol function pressed key config. - 0xFFE9E4E4, 0xFFB2ADAD, 0, 0xFF1E1E1E, - - // Symbol scroll indicator config. - 0xFF73DAF7, 0xFF53B6EE, - - // Symbol minor category tab config. - 0xFF33B5E5, 0x8033B5E5, - - // Symbol candidate background config. - 0xFFFEFEFE, 0xFFECECEC, 0xFFFFFFFF, 0x7C666666, - - // Three dots - 0xFFDDDDDD, - - // Window background resource. - R.drawable.window__background) { - @Override - public void apply(Paint paint, int category) { - Preconditions.checkNotNull(paint); - - // At the moment, caller has responsibility to reset paint. - // TODO(hidehiko): move all style based logic from MozcDrawableFactory to here. - switch (category) { - case STYLE_CATEGORY_KEYICON_MAIN: - case STYLE_CATEGORY_KEYICON_FUNCTION_DARK: - case STYLE_CATEGORY_KEYICON_POPUP_FUNCTION: - paint.setStyle(Style.FILL); - paint.setColor(0xFF272727); - break; - case STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFFFFF); - paint.setShadowLayer(0.5f, 0.f, -1.f, 0xFF404040); - break; - case STYLE_CATEGORY_KEYICON_GUIDE: - case STYLE_CATEGORY_SYMBOL_MAJOR: - paint.setStyle(Style.FILL); - paint.setColor(0xFF333333); - break; - case STYLE_CATEGORY_KEYICON_GUIDE_LIGHT: - paint.setStyle(Style.FILL); - paint.setColor(0xFF999999); - break; - case STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT: - case STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT: - case STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED: - case STYLE_CATEGORY_SYMBOL_MINOR: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFFFFF); - break; - case STYLE_CATEGORY_KEYICON_BOUND: - paint.setStyle(Style.STROKE); - paint.setColor(0xFFCCCCCC); - break; - case STYLE_CATEGORY_KEYICON_FUNCTION: - paint.setStyle(Style.FILL); - paint.setColor(0xFFDDDDDD); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case SkinType.STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW: - paint.setStyle(Style.FILL); - paint.setColor(0xFF55C6EE); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case SkinType.STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW: - paint.setStyle(Style.FILL); - paint.setColor(0xFFC1F300); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT: - paint.setStyle(Style.FILL); - paint.setColor(0xFF57B8E5); - break; - case STYLE_CATEGORY_SYMBOL_MINOR_SELECTED: - paint.setStyle(Style.FILL); - paint.setColor(0xFF63CFFF); - break; - case STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND: - paint.setStyle(Style.FILL); - paint.setColor(0x8C676767); - break; - default: - throw new IllegalStateException("Unknown category: " + category); - } - } - }, - - BLUE_DARKGRAY( - // 12keys layout regular released key config. - 0xFF656565, 0xFF656565, 0xFF8E8E8E, 0xFF535353, 0xFF535353, 0xFF1E1E1E, - - // 12keys layout regular pressed key config. - 0xFFAAAAAA, 0xFF828282, 0, 0, 0, 0, - - // 12keys layout function released key config. - 0xFF222222, 0xFF222222, 0xFF535353, 0xFF000000, 0xFF000000, 0xFF1E1E1E, - - // 12keys layout function pressed key config. - 0xFF066696, 0xDD066696, 0, 0, 0, 0, - - // Qwerty layout regular released key config. - 0xFF656565, 0xFF656565, 0xFF8E8E8E, 0xFF1E1E1E, - - // Qwerty layout regular pressed key config. - 0xFF066696, 0xDD066696, 0, 0xFF1E1E1E, - - // Qwerty layout function released key config. - 0xFF222222, 0xFF222222, 0xFF535353, 0xFF1E1E1E, - - // Qwerty layout function pressed key config. - 0xFF066696, 0xDD066696, 0, 0xFF1E1E1E, - - // Flick color config. - // According to the original design mock, the base color is orange for now. - // The shade is 25% alpha-black. The following value is offline-calculated to mix base color. - 0xFF1B78A3, 0xFF145A7A, - - // Qwerty light on/off sign config. - 0xFF73DAF7, 0xFF3393E5, 0xFF53B6EE, 0xFF333333, 0xFF333333, 0xFF000000, - - // Qwerty round corner radius. - 2.0f, - - // Framed popup window config. - 0xFF066696, 0xDD066696, 0, 0, 0xFF1E1E1E, - - // Candidate scroll indicator config. - 0xFF73DAF7, 0xFF3393E5, - - // Candidate background config. - 0xFFE8E8E8, 0xFFDCDCDC, 0xFFFFFFFF, 0xFF8C8C8C, - - // Candidate focused background config. - 0xFFA5DDF6, 0xFFC1E9F5, 0x40000000, - - // Symbol function released key config. - 0xFF656565, 0xFF656565, 0xFF8E8E8E, 0xFF1E1E1E, - - // Symbol function pressed key config. - 0xFF066696, 0xDD066696, 0, 0xFF1E1E1E, - - // Symbol scroll indicator config. - 0xFF73DAF7, 0xFF53B6EE, - - // Symbol minor category tab config. - 0xFF33B5E5, 0x8033B5E5, - - // Symbol candidate background config. - 0xFFFEFEFE, 0xFFECECEC, 0xFFFFFFFF, 0x7C666666, - - // Three dots - 0xFFF7F7F7, - - // Window background resource. - R.drawable.window_background_black) { - @Override - public void apply(Paint paint, int category) { - Preconditions.checkNotNull(paint); - - // At the moment, caller has responsibility to reset paint. - // TODO(hidehiko): move all style based logic from MozcDrawableFactory to here. - switch (category) { - case STYLE_CATEGORY_KEYICON_MAIN: - case STYLE_CATEGORY_KEYICON_POPUP_FUNCTION: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFFFFF); - break; - case STYLE_CATEGORY_KEYICON_FUNCTION_DARK: - paint.setStyle(Style.FILL); - paint.setColor(0xFF868686); - break; - case STYLE_CATEGORY_KEYICON_GUIDE: - paint.setStyle(Style.FILL); - paint.setColor(0xFFBEBEBE); - break; - case STYLE_CATEGORY_KEYICON_GUIDE_LIGHT: - paint.setStyle(Style.FILL); - paint.setColor(0xFF999999); - break; - case STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT: - case STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT: - case STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED: - case STYLE_CATEGORY_SYMBOL_MINOR: - paint.setStyle(Style.FILL); - paint.setColor(0xFFFFFFFF); - break; - case STYLE_CATEGORY_KEYICON_BOUND: - paint.setStyle(Style.STROKE); - paint.setColor(0xFFCCCCCC); - break; - case STYLE_CATEGORY_KEYICON_FUNCTION: - paint.setStyle(Style.FILL); - paint.setColor(0xFFF7F7F7); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case SkinType.STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW: - paint.setStyle(Style.FILL); - paint.setColor(0xFF55C6EE); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case SkinType.STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW: - paint.setStyle(Style.FILL); - paint.setColor(0xFFC1F300); - paint.setShadowLayer(2.f, 0.f, -1.f, 0xFF404040); - break; - case STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT: - paint.setStyle(Style.FILL); - paint.setColor(0xFF4DA7CF); - break; - case STYLE_CATEGORY_SYMBOL_MAJOR: - case STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK: - paint.setStyle(Style.FILL); - paint.setColor(0xFF333333); - break; - case STYLE_CATEGORY_SYMBOL_MINOR_SELECTED: - paint.setStyle(Style.FILL); - paint.setColor(0xFF63CFFF); - break; - case STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND: - paint.setStyle(Style.FILL); - paint.setColor(0x8C676767); - break; - default: - throw new IllegalStateException("Unknown category: " + category); - } - } - }, + ORANGE_LIGHTGRAY(R.xml.skin_orange_lightgray), + BLUE_LIGHTGRAY(R.xml.skin_blue_lightgray), + BLUE_DARKGRAY(R.xml.skin_blue_darkgray), + MATERIAL_DESIGN_LIGHT(R.xml.skin_material_design_light), + MATERIAL_DESIGN_DARK(R.xml.skin_material_design_dark), // This is an instance for testing of skin support in some classes. - // TODO(hidehiko): remove this value when other skin types (described in above TODO) - // are supported. - TEST(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) { - @Override - public void apply(Paint paint, int category) { - // Do nothing. - } - } + // TODO(matsuzakit): No more required. Remove. + TEST(R.xml.skin_orange_lightgray) ; - public static final int STYLE_CATEGORY_KEYICON_MAIN = 0; - public static final int STYLE_CATEGORY_KEYICON_GUIDE = 1; - public static final int STYLE_CATEGORY_KEYICON_GUIDE_LIGHT = 2; - public static final int STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT = 3; - public static final int STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT = 4; - public static final int STYLE_CATEGORY_KEYICON_BOUND = 5; - public static final int STYLE_CATEGORY_KEYICON_FUNCTION = 6; - public static final int STYLE_CATEGORY_KEYICON_FUNCTION_DARK = 7; - public static final int STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW = 8; - public static final int STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW = 9; - public static final int STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT = 10; - public static final int STYLE_CATEGORY_KEYICON_POPUP_FUNCTION = 11; - public static final int STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK = 12; - public static final int STYLE_CATEGORY_SYMBOL_MAJOR = 13; - public static final int STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED = 14; - public static final int STYLE_CATEGORY_SYMBOL_MINOR = 15; - public static final int STYLE_CATEGORY_SYMBOL_MINOR_SELECTED = 16; - public static final int STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND = 17; - - public final int twelvekeysLayoutReleasedKeyTopColor; - public final int twelvekeysLayoutReleasedKeyBottomColor; - public final int twelvekeysLayoutReleasedKeyHighlightColor; - public final int twelvekeysLayoutReleasedKeyLightShadeColor; - public final int twelvekeysLayoutReleasedKeyDarkShadeColor; - public final int twelvekeysLayoutReleasedKeyShadowColor; - - public final int twelvekeysLayoutPressedKeyTopColor; - public final int twelvekeysLayoutPressedKeyBottomColor; - public final int twelvekeysLayoutPressedKeyHighlightColor; - public final int twelvekeysLayoutPressedKeyLightShadeColor; - public final int twelvekeysLayoutPressedKeyDarkShadeColor; - public final int twelvekeysLayoutPressedKeyShadowColor; - - public final int twelvekeysLayoutReleasedFunctionKeyTopColor; - public final int twelvekeysLayoutReleasedFunctionKeyBottomColor; - public final int twelvekeysLayoutReleasedFunctionKeyHighlightColor; - public final int twelvekeysLayoutReleasedFunctionKeyLightShadeColor; - public final int twelvekeysLayoutReleasedFunctionKeyDarkShadeColor; - public final int twelvekeysLayoutReleasedFunctionKeyShadowColor; - - public final int twelvekeysLayoutPressedFunctionKeyTopColor; - public final int twelvekeysLayoutPressedFunctionKeyBottomColor; - public final int twelvekeysLayoutPressedFunctionKeyHighlightColor; - public final int twelvekeysLayoutPressedFunctionKeyLightShadeColor; - public final int twelvekeysLayoutPressedFunctionKeyDarkShadeColor; - public final int twelvekeysLayoutPressedFunctionKeyShadowColor; - - public final int qwertyLayoutReleasedKeyTopColor; - public final int qwertyLayoutReleasedKeyBottomColor; - public final int qwertyLayoutReleasedKeyHighlightColor; - public final int qwertyLayoutReleasedKeyShadowColor; - - public final int qwertyLayoutPressedKeyTopColor; - public final int qwertyLayoutPressedKeyBottomColor; - public final int qwertyLayoutPressedKeyHighlightColor; - public final int qwertyLayoutPressedKeyShadowColor; - - public final int qwertyLayoutReleasedFunctionKeyTopColor; - public final int qwertyLayoutReleasedFunctionKeyBottomColor; - public final int qwertyLayoutReleasedFunctionKeyHighlightColor; - public final int qwertyLayoutReleasedFunctionKeyShadowColor; - - public final int qwertyLayoutPressedFunctionKeyTopColor; - public final int qwertyLayoutPressedFunctionKeyBottomColor; - public final int qwertyLayoutPressedFunctionKeyHighlightColor; - public final int qwertyLayoutPressedFunctionKeyShadowColor; - - public final int flickBaseColor; - public final int flickShadeColor; - - public final int qwertyLightOnSignLightColor; - public final int qwertyLightOnSignDarkColor; - public final int qwertyLightOnSignShadeColor; - - public final int qwertyLightOffSignLightColor; - public final int qwertyLightOffSignDarkColor; - public final int qwertyLightOffSignShadeColor; - - public final float qwertyKeyRoundRadius; - - public final int popupFrameWindowTopColor; - public final int popupFrameWindowBottomColor; - public final int popupFrameWindowBorderColor; - public final int popupFrameWindowInnerPaneColor; - public final int popupFrameWindowShadowColor; - - public final int candidateScrollBarTopColor; - public final int candidateScrollBarBottomColor; - public final int candidateBackgroundTopColor; - public final int candidateBackgroundBottomColor; - public final int candidateBackgroundHighlightColor; - public final int candidateBackgroundBorderColor; - public final int candidateBackgroundFocusedTopColor; - public final int candidateBackgroundFocusedBottomColor; - public final int candidateBackgroundFocusedShadowColor; + private Optional skin = Optional.absent(); + private final int resourceId; - public final int symbolReleasedFunctionKeyTopColor; - public final int symbolReleasedFunctionKeyBottomColor; - public final int symbolReleasedFunctionKeyHighlightColor; - public final int symbolReleasedFunctionKeyShadowColor; - - public final int symbolPressedFunctionKeyTopColor; - public final int symbolPressedFunctionKeyBottomColor; - public final int symbolPressedFunctionKeyHighlightColor; - public final int symbolPressedFunctionKeyShadowColor; - - public final int symbolScrollBarTopColor; - public final int symbolScrollBarBottomColor; - - public final int symbolMinorCategoryTabSelectedColor; - public final int symbolMinorCategoryTabPressedColor; - - public final int symbolCandidateBackgroundTopColor; - public final int symbolCandidateBackgroundBottomColor; - public final int symbolCandidateBackgroundHighlightColor; - public final int symbolCandidateBackgroundBorderColor; - - public final int threeDotsColor; - - public final int windowBackgroundResourceId; - - public abstract void apply(Paint paint, int category); - - private SkinType( - int twelvekeysLayoutReleasedKeyTopColor, - int twelvekeysLayoutReleasedKeyBottomColor, - int twelvekeysLayoutReleasedKeyHighlightColor, - int twelvekeysLayoutReleasedKeyLightShadeColor, - int twelvekeysLayoutReleasedKeyDarkShadeColor, - int twelvekeysLayoutReleasedKeyShadowColor, - int twelvekeysLayoutPressedKeyTopColor, - int twelvekeysLayoutPressedKeyBottomColor, - int twelvekeysLayoutPressedKeyHighlightColor, - int twelvekeysLayoutPressedKeyLightShadeColor, - int twelvekeysLayoutPressedKeyDarkShadeColor, - int twelvekeysLayoutPressedKeyShadowColor, - int twelvekeysLayoutReleasedFunctionKeyTopColor, - int twelvekeysLayoutReleasedFunctionKeyBottomColor, - int twelvekeysLayoutReleasedFunctionKeyHighlightColor, - int twelvekeysLayoutReleasedFunctionKeyLightShadeColor, - int twelvekeysLayoutReleasedFunctionKeyDarkShadeColor, - int twelvekeysLayoutReleasedFunctionKeyShadowColor, - int twelvekeysLayoutPressedFunctionKeyTopColor, - int twelvekeysLayoutPressedFunctionKeyBottomColor, - int twelvekeysLayoutPressedFunctionKeyHighlightColor, - int twelvekeysLayoutPressedFunctionKeyLightShadeColor, - int twelvekeysLayoutPressedFunctionKeyDarkShadeColor, - int twelvekeysLayoutPressedFunctionKeyShadowColor, - int qwertyLayoutReleasedKeyTopColor, - int qwertyLayoutReleasedKeyBottomColor, - int qwertyLayoutReleasedKeyHighlightColor, - int qwertyLayoutReleasedKeyShadowColor, - int qwertyLayoutPressedKeyTopColor, - int qwertyLayoutPressedKeyBottomColor, - int qwertyLayoutPressedKeyHighlightColor, - int qwertyLayoutPressedKeyShadowColor, - int qwertyLayoutReleasedFunctionKeyTopColor, - int qwertyLayoutReleasedFunctionKeyBottomColor, - int qwertyLayoutReleasedFunctionKeyHighlightColor, - int qwertyLayoutReleasedFunctionKeyShadowColor, - int qwertyLayoutPressedFunctionKeyTopColor, - int qwertyLayoutPressedFunctionKeyBottomColor, - int qwertyLayoutPressedFunctionKeyHighlightColor, - int qwertyLayoutPressedFunctionKeyShadowColor, - int flickBaseColor, - int flickShadeColor, - int qwertyLightOnSignLightColor, - int qwertyLightOnSignDarkColor, - int qwertyLightOnSignShadeColor, - int qwertyLightOffSignLightColor, - int qwertyLightOffSignDarkColor, - int qwertyLightOffSignShadeColor, - float qwertyKeyRoundRadius, - int popupFrameWindowTopColor, - int popupFrameWindowBottomColor, - int popupFrameWindowBorderColor, - int popupFrameWindowInnerPaneColor, - int popupFrameWindowShadowColor, - int candidateScrollBarTopColor, - int candidateScrollBarBottomColor, - int candidateBackgroundTopColor, - int candidateBackgroundBottomColor, - int candidateBackgroundHighlightColor, - int candidateBackgroundBorderColor, - int candidateBackgroundFocusedTopColor, - int candidateBackgroundFocusedBottomColor, - int candidateBackgroundFocusedShadowColor, - int symbolReleasedFunctionKeyTopColor, - int symbolReleasedFunctionKeyBottomColor, - int symbolReleasedFunctionKeyHighlightColor, - int symbolReleasedFunctionKeyShadowColor, - int symbolPressedFunctionKeyTopColor, - int symbolPressedFunctionKeyBottomColor, - int symbolPressedFunctionKeyHighlightColor, - int symbolPressedFunctionKeyShadowColor, - int symbolScrollBarTopColor, - int symbolScrollBarBottomColor, - int symbolMinorCategoryTabSelectedColor, - int symbolMinorCategoryTabPressedColor, - int symbolCandidateBackgroundTopColor, - int symbolCandidateBackgroundBottomColor, - int symbolCandidateBackgroundHighlightColor, - int symbolCandidateBackgroundBorderColor, - int threeDotsColor, - int windowBackgroundResourceId) { - this.twelvekeysLayoutReleasedKeyTopColor = twelvekeysLayoutReleasedKeyTopColor; - this.twelvekeysLayoutReleasedKeyBottomColor = twelvekeysLayoutReleasedKeyBottomColor; - this.twelvekeysLayoutReleasedKeyHighlightColor = twelvekeysLayoutReleasedKeyHighlightColor; - this.twelvekeysLayoutReleasedKeyLightShadeColor = twelvekeysLayoutReleasedKeyLightShadeColor; - this.twelvekeysLayoutReleasedKeyDarkShadeColor = twelvekeysLayoutReleasedKeyDarkShadeColor; - this.twelvekeysLayoutReleasedKeyShadowColor = twelvekeysLayoutReleasedKeyShadowColor; - - this.twelvekeysLayoutPressedKeyTopColor = twelvekeysLayoutPressedKeyTopColor; - this.twelvekeysLayoutPressedKeyBottomColor = twelvekeysLayoutPressedKeyBottomColor; - this.twelvekeysLayoutPressedKeyHighlightColor = twelvekeysLayoutPressedKeyHighlightColor; - this.twelvekeysLayoutPressedKeyLightShadeColor = twelvekeysLayoutPressedKeyLightShadeColor; - this.twelvekeysLayoutPressedKeyDarkShadeColor = twelvekeysLayoutPressedKeyDarkShadeColor; - this.twelvekeysLayoutPressedKeyShadowColor = twelvekeysLayoutPressedKeyShadowColor; - - this.twelvekeysLayoutReleasedFunctionKeyTopColor = twelvekeysLayoutReleasedFunctionKeyTopColor; - this.twelvekeysLayoutReleasedFunctionKeyBottomColor = - twelvekeysLayoutReleasedFunctionKeyBottomColor; - this.twelvekeysLayoutReleasedFunctionKeyHighlightColor = - twelvekeysLayoutReleasedFunctionKeyHighlightColor; - this.twelvekeysLayoutReleasedFunctionKeyLightShadeColor = - twelvekeysLayoutReleasedFunctionKeyLightShadeColor; - this.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor = - twelvekeysLayoutReleasedFunctionKeyDarkShadeColor; - this.twelvekeysLayoutReleasedFunctionKeyShadowColor = - twelvekeysLayoutReleasedFunctionKeyShadowColor; - - this.twelvekeysLayoutPressedFunctionKeyTopColor = twelvekeysLayoutPressedFunctionKeyTopColor; - this.twelvekeysLayoutPressedFunctionKeyBottomColor = - twelvekeysLayoutPressedFunctionKeyBottomColor; - this.twelvekeysLayoutPressedFunctionKeyHighlightColor = - twelvekeysLayoutPressedFunctionKeyHighlightColor; - this.twelvekeysLayoutPressedFunctionKeyLightShadeColor = - twelvekeysLayoutPressedFunctionKeyLightShadeColor; - this.twelvekeysLayoutPressedFunctionKeyDarkShadeColor = - twelvekeysLayoutPressedFunctionKeyDarkShadeColor; - this.twelvekeysLayoutPressedFunctionKeyShadowColor = - twelvekeysLayoutPressedFunctionKeyShadowColor; - - this.qwertyLayoutReleasedKeyTopColor = qwertyLayoutReleasedKeyTopColor; - this.qwertyLayoutReleasedKeyBottomColor = qwertyLayoutReleasedKeyBottomColor; - this.qwertyLayoutReleasedKeyHighlightColor = qwertyLayoutReleasedKeyHighlightColor; - this.qwertyLayoutReleasedKeyShadowColor = qwertyLayoutReleasedKeyShadowColor; - - this.qwertyLayoutPressedKeyTopColor = qwertyLayoutPressedKeyTopColor; - this.qwertyLayoutPressedKeyBottomColor = qwertyLayoutPressedKeyBottomColor; - this.qwertyLayoutPressedKeyHighlightColor = qwertyLayoutPressedKeyHighlightColor; - this.qwertyLayoutPressedKeyShadowColor = qwertyLayoutPressedKeyShadowColor; - - this.qwertyLayoutReleasedFunctionKeyTopColor = qwertyLayoutReleasedFunctionKeyTopColor; - this.qwertyLayoutReleasedFunctionKeyBottomColor = qwertyLayoutReleasedFunctionKeyBottomColor; - this.qwertyLayoutReleasedFunctionKeyHighlightColor = - qwertyLayoutReleasedFunctionKeyHighlightColor; - this.qwertyLayoutReleasedFunctionKeyShadowColor = qwertyLayoutReleasedFunctionKeyShadowColor; - - this.qwertyLayoutPressedFunctionKeyTopColor = qwertyLayoutPressedFunctionKeyTopColor; - this.qwertyLayoutPressedFunctionKeyBottomColor = qwertyLayoutPressedFunctionKeyBottomColor; - this.qwertyLayoutPressedFunctionKeyHighlightColor = - qwertyLayoutPressedFunctionKeyHighlightColor; - this.qwertyLayoutPressedFunctionKeyShadowColor = qwertyLayoutPressedFunctionKeyShadowColor; - - this.flickBaseColor = flickBaseColor; - this.flickShadeColor = flickShadeColor; - - this.qwertyLightOnSignLightColor = qwertyLightOnSignLightColor; - this.qwertyLightOnSignDarkColor = qwertyLightOnSignDarkColor; - this.qwertyLightOnSignShadeColor = qwertyLightOnSignShadeColor; - - this.qwertyLightOffSignLightColor = qwertyLightOffSignLightColor; - this.qwertyLightOffSignDarkColor = qwertyLightOffSignDarkColor; - this.qwertyLightOffSignShadeColor = qwertyLightOffSignShadeColor; - - this.qwertyKeyRoundRadius = qwertyKeyRoundRadius; - - this.popupFrameWindowTopColor = popupFrameWindowTopColor; - this.popupFrameWindowBottomColor = popupFrameWindowBottomColor; - this.popupFrameWindowBorderColor = popupFrameWindowBorderColor; - this.popupFrameWindowInnerPaneColor = popupFrameWindowInnerPaneColor; - this.popupFrameWindowShadowColor = popupFrameWindowShadowColor; - - this.candidateScrollBarTopColor = candidateScrollBarTopColor; - this.candidateScrollBarBottomColor = candidateScrollBarBottomColor; - this.candidateBackgroundTopColor = candidateBackgroundTopColor; - this.candidateBackgroundBottomColor = candidateBackgroundBottomColor; - this.candidateBackgroundHighlightColor = candidateBackgroundHighlightColor; - this.candidateBackgroundBorderColor = candidateBackgroundBorderColor; - this.candidateBackgroundFocusedTopColor = candidateBackgroundFocusedTopColor; - this.candidateBackgroundFocusedBottomColor = candidateBackgroundFocusedBottomColor; - this.candidateBackgroundFocusedShadowColor = candidateBackgroundFocusedShadowColor; - - this.symbolReleasedFunctionKeyTopColor = symbolReleasedFunctionKeyTopColor; - this.symbolReleasedFunctionKeyBottomColor = symbolReleasedFunctionKeyBottomColor; - this.symbolReleasedFunctionKeyHighlightColor = symbolReleasedFunctionKeyHighlightColor; - this.symbolReleasedFunctionKeyShadowColor = symbolReleasedFunctionKeyShadowColor; - this.symbolPressedFunctionKeyTopColor = symbolPressedFunctionKeyTopColor; - this.symbolPressedFunctionKeyBottomColor = symbolPressedFunctionKeyBottomColor; - this.symbolPressedFunctionKeyHighlightColor = symbolPressedFunctionKeyHighlightColor; - this.symbolPressedFunctionKeyShadowColor = symbolPressedFunctionKeyShadowColor; - - this.symbolScrollBarTopColor = symbolScrollBarTopColor; - this.symbolScrollBarBottomColor = symbolScrollBarBottomColor; - - this.symbolMinorCategoryTabSelectedColor = symbolMinorCategoryTabSelectedColor; - this.symbolMinorCategoryTabPressedColor = symbolMinorCategoryTabPressedColor; - - this.symbolCandidateBackgroundTopColor = symbolCandidateBackgroundTopColor; - this.symbolCandidateBackgroundBottomColor = symbolCandidateBackgroundBottomColor; - this.symbolCandidateBackgroundHighlightColor = symbolCandidateBackgroundHighlightColor; - this.symbolCandidateBackgroundBorderColor = symbolCandidateBackgroundBorderColor; - - this.threeDotsColor = threeDotsColor; + private SkinType(int resourceId) { + this.resourceId = resourceId; + } - this.windowBackgroundResourceId = windowBackgroundResourceId; + public Skin getSkin(Resources resources) { + Preconditions.checkNotNull(resources); + if (skin.isPresent()) { + return skin.get(); + } + SkinParser parser = new SkinParser(resources, resources.getXml(resourceId)); + try { + skin = Optional.of(parser.parseSkin()); + } catch (SkinParserException e) { + MozcLog.e(e.getLocalizedMessage()); + skin = Optional.of(new Skin()); // Fall-back skin. + } + return skin.get(); } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/SymbolMajorCategoryButtonDrawableFactory.java b/src/android/src/com/google/android/inputmethod/japanese/view/SymbolMajorCategoryButtonDrawableFactory.java index 2ed0987c4..133c648ae 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/SymbolMajorCategoryButtonDrawableFactory.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/SymbolMajorCategoryButtonDrawableFactory.java @@ -34,6 +34,7 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; @@ -57,20 +58,23 @@ private interface PathFactory { } private static class LeftButtonPathFactory implements PathFactory { - + private final float padding; private final float round; - LeftButtonPathFactory(float round) { + // Padding is applied to left, top and bottom. + // Right doesn't have padding in order to center left/right buttons. + LeftButtonPathFactory(float padding, float round) { + this.padding = padding; this.round = round; } @Override public Path newInstance(Rect bounds) { Preconditions.checkNotNull(bounds); - float left = bounds.left; - float top = bounds.top; + float left = bounds.left + padding; + float top = bounds.top + padding; float right = bounds.right - 2; - float bottom = bounds.bottom - 1; + float bottom = bounds.bottom - 1 - padding; Path path = new Path(); path.moveTo(right, bottom); @@ -84,14 +88,20 @@ public Path newInstance(Rect bounds) { } private static class CenterButtonPathFactory implements PathFactory { + private final float padding; + + // Padding is applied only to top and bottom. + CenterButtonPathFactory(float padding) { + this.padding = padding; + } @Override public Path newInstance(Rect bounds) { Preconditions.checkNotNull(bounds); float left = bounds.left; - float top = bounds.top; + float top = bounds.top + padding; float right = bounds.right - 2; - float bottom = bounds.bottom - 1; + float bottom = bounds.bottom - 1 - padding; Path path = new Path(); path.addRect(left, top, right, bottom, Direction.CW); @@ -100,9 +110,13 @@ public Path newInstance(Rect bounds) { } private static class RightButtonPathFactory implements PathFactory { + private final float padding; private final float round; - RightButtonPathFactory(float round) { + // Padding is applied to right, top and bottom. + // Left doesn't have padding in order to center left/right buttons. + RightButtonPathFactory(float padding, float round) { + this.padding = padding; this.round = round; } @@ -110,9 +124,9 @@ private static class RightButtonPathFactory implements PathFactory { public Path newInstance(Rect bounds) { Preconditions.checkNotNull(bounds); float left = bounds.left; - float top = bounds.top; - float right = bounds.right - 1; - float bottom = bounds.bottom - 1; + float top = bounds.top + padding; + float right = bounds.right - 1 - padding; + float bottom = bounds.bottom - 1 - padding; Path path = new Path(); path.moveTo(left, top); @@ -190,10 +204,14 @@ protected void onBoundsChange(Rect bounds) { private static class EmojiDisableIconDrawable extends BaseBackgroundDrawable { + private final int size; private final Drawable sourceDrawable; - EmojiDisableIconDrawable(Drawable sourceDrawable) { + EmojiDisableIconDrawable(Resources resources, Drawable sourceDrawable) { super(0, 0, 0, 0); + size = Preconditions.checkNotNull(resources).getDimensionPixelSize( + R.dimen.symbol_major_emoji_disable_icon_height); + sourceDrawable.setBounds(0, 0, size, size); this.sourceDrawable = Preconditions.checkNotNull(sourceDrawable); } @@ -201,9 +219,6 @@ private static class EmojiDisableIconDrawable extends BaseBackgroundDrawable { public void draw(Canvas canvas) { Rect bounds = getBounds(); - // Heuristically, the size is 1/3 of the height. - int size = bounds.height() / 3; - sourceDrawable.setBounds(0, 0, size, size); int saveCount = canvas.save(); try { @@ -216,56 +231,55 @@ public void draw(Canvas canvas) { } } - private final MozcDrawableFactory factory; - - private final int topColor; - private final int bottomColor; - private final int pressedTopColor; - private final int pressedBottomColor; - private final int shadowColor; - - private final PathFactory leftButtonPathFactory; - private final PathFactory centerButtonPathFactory; - private final PathFactory rightButtonPathFactory; - - public SymbolMajorCategoryButtonDrawableFactory( - MozcDrawableFactory factory, int topColor, int bottomColor, - int pressedTopColor, int pressedBottomColor, int shadowColor, - float round) { - this.factory = Preconditions.checkNotNull(factory); - this.topColor = topColor; - this.bottomColor = bottomColor; - this.pressedTopColor = pressedTopColor; - this.pressedBottomColor = pressedBottomColor; - this.shadowColor = shadowColor; - - this.leftButtonPathFactory = new LeftButtonPathFactory(round); - this.centerButtonPathFactory = new CenterButtonPathFactory(); - this.rightButtonPathFactory = new RightButtonPathFactory(round); + private Skin skin = Skin.getFallbackInstance(); + + private final Resources resources; + + public SymbolMajorCategoryButtonDrawableFactory(Resources resources) { + this.resources = Preconditions.checkNotNull(resources); } public Drawable createLeftButtonDrawable() { - return BackgroundDrawableFactory.createSelectableDrawable( - new ButtonDrawable(leftButtonPathFactory, pressedTopColor, pressedBottomColor, 0), - new ButtonDrawable(leftButtonPathFactory, topColor, bottomColor, shadowColor)); + return createSelectableDrawableWithPathFactory( + new LeftButtonPathFactory(skin.symbolMajorButtonPaddingDimension, + skin.symbolMajorButtonRoundDimension)); } public Drawable createCenterButtonDrawable() { - return BackgroundDrawableFactory.createSelectableDrawable( - new ButtonDrawable(centerButtonPathFactory, pressedTopColor, pressedBottomColor, 0), - new ButtonDrawable(centerButtonPathFactory, topColor, bottomColor, shadowColor)); + return createSelectableDrawableWithPathFactory( + new CenterButtonPathFactory(skin.symbolMajorButtonPaddingDimension)); } public Drawable createRightButtonDrawable(boolean emojiEnabled) { - Drawable drawable = BackgroundDrawableFactory.createSelectableDrawable( - new ButtonDrawable(rightButtonPathFactory, pressedTopColor, pressedBottomColor, 0), - new ButtonDrawable(rightButtonPathFactory, topColor, bottomColor, shadowColor)); + Drawable drawable = createSelectableDrawableWithPathFactory( + new RightButtonPathFactory(skin.symbolMajorButtonPaddingDimension, + skin.symbolMajorButtonRoundDimension)); if (emojiEnabled) { return drawable; } return new LayerDrawable(new Drawable[] { drawable, - new EmojiDisableIconDrawable(factory.getDrawable(R.raw.emoji_disable_icon).orNull()), + new EmojiDisableIconDrawable( + resources, skin.getDrawable(resources, R.raw.emoji_disable_icon)), }); } + + private Drawable createSelectableDrawableWithPathFactory(PathFactory pathFactory) { + return BackgroundDrawableFactory.createSelectableDrawable( + new ButtonDrawable(pathFactory, + skin.symbolMajorButtonSelectedTopColor, + skin.symbolMajorButtonSelectedBottomColor, 0), + Optional.of(BackgroundDrawableFactory.createPressableDrawable( + new ButtonDrawable(pathFactory, + skin.symbolMajorButtonPressedTopColor, + skin.symbolMajorButtonPressedBottomColor, 0), + Optional.of(new ButtonDrawable(pathFactory, + skin.symbolMajorButtonTopColor, + skin.symbolMajorButtonBottomColor, + skin.symbolMajorButtonShadowColor))))); + } + + public void setSkin(Skin skin) { + this.skin = Preconditions.checkNotNull(skin); + } } diff --git a/src/android/src/com/google/android/inputmethod/japanese/view/TabSelectedBackgroundDrawable.java b/src/android/src/com/google/android/inputmethod/japanese/view/TabSelectedBackgroundDrawable.java index c202471a3..23bd47028 100644 --- a/src/android/src/com/google/android/inputmethod/japanese/view/TabSelectedBackgroundDrawable.java +++ b/src/android/src/com/google/android/inputmethod/japanese/view/TabSelectedBackgroundDrawable.java @@ -31,6 +31,7 @@ import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; /** * Selected tab highlight implementation. @@ -54,6 +55,7 @@ public void draw(Canvas canvas) { } // Paint the rectangle. - canvas.drawRect(0, 0, getBounds().right, highlightHeight, paint); + Rect bounds = getBounds(); + canvas.drawRect(0, bounds.bottom - highlightHeight, bounds.right, bounds.bottom, paint); } } diff --git a/src/android/static_resources/resources_oss/res/drawable-hdpi/application_icon.png b/src/android/static_resources/application_icon/oss_icon/drawable-hdpi/application_icon.png similarity index 100% rename from src/android/static_resources/resources_oss/res/drawable-hdpi/application_icon.png rename to src/android/static_resources/application_icon/oss_icon/drawable-hdpi/application_icon.png diff --git a/src/android/static_resources/application_icon/oss_icon/drawable-mdpi/application_icon.png b/src/android/static_resources/application_icon/oss_icon/drawable-mdpi/application_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..47689b60c0834f99f98e0dcbbec4e5999ead4e2b GIT binary patch literal 3774 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 literal 0 HcmV?d00001 diff --git a/src/android/static_resources/resources_oss/res/drawable-xhdpi/application_icon.png b/src/android/static_resources/application_icon/oss_icon/drawable-xhdpi/application_icon.png similarity index 100% rename from src/android/static_resources/resources_oss/res/drawable-xhdpi/application_icon.png rename to src/android/static_resources/application_icon/oss_icon/drawable-xhdpi/application_icon.png diff --git a/src/android/static_resources/application_icon/oss_icon/drawable-xxhdpi/application_icon.png b/src/android/static_resources/application_icon/oss_icon/drawable-xxhdpi/application_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f3f30559e659d397e8e6baa35a2d3fc88f2cc0ee GIT binary patch literal 10508 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 C5;r~o literal 0 HcmV?d00001 diff --git a/src/android/static_resources/application_icon/oss_icon/drawable-xxxhdpi/application_icon.png b/src/android/static_resources/application_icon/oss_icon/drawable-xxxhdpi/application_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2b6f59c9a2657680340fcaf1ecf479eba5c3d5d8 GIT binary patch literal 35940 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 P00000NkvXXu0mjf_<71Z literal 0 HcmV?d00001 diff --git a/src/android/tests/res/values/dummy.xml b/src/android/static_resources/launcher_icon_resources/launcher_icon_preinstall_bools.xml similarity index 92% rename from src/android/tests/res/values/dummy.xml rename to src/android/static_resources/launcher_icon_resources/launcher_icon_preinstall_bools.xml index f2315c061..0cf1ea8e3 100644 --- a/src/android/tests/res/values/dummy.xml +++ b/src/android/static_resources/launcher_icon_resources/launcher_icon_preinstall_bools.xml @@ -29,6 +29,8 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - - + + + false + + diff --git a/src/android/static_resources/resources_oss/res/drawable/dropshadow_right.xml b/src/android/static_resources/launcher_icon_resources/launcher_icon_standard_bools.xml similarity index 87% rename from src/android/static_resources/resources_oss/res/drawable/dropshadow_right.xml rename to src/android/static_resources/launcher_icon_resources/launcher_icon_standard_bools.xml index 97527b3da..0675dd7a8 100644 --- a/src/android/static_resources/resources_oss/res/drawable/dropshadow_right.xml +++ b/src/android/static_resources/launcher_icon_resources/launcher_icon_standard_bools.xml @@ -29,8 +29,8 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - - - + + + true + + diff --git a/src/android/static_resources/resources_oss/res/drawable-hdpi/status_icon_image_alphabet.png b/src/android/static_resources/resources_oss/res/drawable-hdpi/status_icon_image_alphabet.png index 727894f2cc1bf66420df16fa2f926ea314904b8f..da1b4f1972fbd73df1ed9422dad6e38be90c1baa 100644 GIT binary patch delta 405 zcmV;G0c!rb2E+r9Bpd;AQb$4nuFf3k00006VoOIv0RI600RN!9r<0K}7=H(JNliru z-U%K7AOa(Px|;w10YpheK~z}7?Up}F!%!5(&uu9p{@Vn*sI#M+h=btb;@T>X4&o>9 zTlgu&$zl~9#7WdyI*Egmc2H0XqEhhZ;LQ?cqr zfE&KK?k8ijZmQ*V=C98z{C|_TRj=D#*ZLu?B5)7f0wv%DxC1Qj#1~@_n)c0_Kp{mt3VgVJve)0Sir0NJ`~2|* zHfu>b^V@2#6Ki+^iyi7WT;B-4)PX=C5J=_=p=3H^go$ra00000NkvXXu0mjfeciql delta 782 zcmV+p1M&RC1G)x~Bn<>}LP=Bz2nYy#2xN$nKp1}w(EZ*psMAVX6%akb+%0000KbVXQnLvm$dbZKvHAXI5>WdJcUE;cSW{Mjyh0007% zNkl4-Vj2IbwhB;+49-I|9R6>?lB*zIux59eBM1-yMOy zfg>~ATZ+9Av8Jx`PQVW0xhLE|B=5Cass&b@#+@ZoL*D4W;orix4R6jX7~aO7(s&$S z;2b7O_C0meSr_auUMQ{`xQfs49UhdP+oxvKDuYGDix?`I9%3w|bPJd9X8{Xa<-LD4 z_h2|~dMWq|KNQzD@Uy9X2CqfLz(6seoS48wzS*U6gAI9p^xFMH>Q&=ZQ za1zhsKlB#Q4arga_NjwK#2{X3F|U7?55rNp4K?IQEiXaA7K~H@tCGqO{|BsJj?{(% za*g~7w6ewbGVF_p740roC6yzlr*RzDzJrW*YweAF&jPdi`Y5%DQbo2p;`;ABc^#zi7x1HP0a-PA8QkWzNKGuxW8 z?y-n?2Je|(RIlOoljfzA?&6~9U0@ZC60Ysg1d#a%X629X==kI|h5l z)a!FoV@3O!O~1+qq^Tacv2UDhVq1G`A+%^ec)MU#)dj1nE?8AP2Iw=3*~i0K2><{9 M07*qoM6N<$f@+LwL;wH) diff --git a/src/android/static_resources/resources_oss/res/drawable-hdpi/status_icon_image_hiragana.png b/src/android/static_resources/resources_oss/res/drawable-hdpi/status_icon_image_hiragana.png index 990fc93af66a2e2280c31df3154ba24c080822d2..672aa8e7b6cca862cb58e762aff1fde05a3d2ded 100644 GIT binary patch delta 635 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 delta 1181 zcmV;O1Y-NG2E_@GBn<>}LP=Bz2nYy#2xN$nKp1}w(EZ*psMAVX6%akb+%0000KbVXQnLvm$dbZKvHAXI5>WdJcUE;cSW{Mjyh000CZ zNkl>p~u=e3>$CI%4jGTn|!{+R@_Bv~S z|Goa-Wvz~ZfdS_-Mz!03p<;wUyD>tb-54RzZj2CUH^vOtL@A}QSc>cL6Sm>oh&ZdM ze%J^Ub1lV!qP+?`a83~zU21<95&f+Orj&nRTyaf6cSQVC=cOH0i@B82C``pM97mu0 zl^5VWTv80Z7oVe}FtP(RL}dbXR$v-FL>Knp5PC5m6HDV3V-dy| zhEB$8`Ku2`UmfGAM_>YOFRn9TafLaL*RTWi2=vR6IVnq`6$Uh=1>LfdNAZU&r(S>T z$6+~Q)j}NtN3a%uV+yX7^T|wHQy6y$@1RE}ycY+HXMbUPMD*4*x{S(B{9i~Z-G@yw znXZVK*J8|*SdNX@ifs{bVkqw&&MApXuG+ma>6nPacz(eT73%F7Clm88YURfe= zK32=&cBbqvQ{_n2z>eXN(XdD;rF4G-p2PhW{`Lbtz?O*EQTN?LB9Kz*z++f}t6K~? zjD50STp@?x!T1p?B4Se=8)_3sDUHWEJkjFu?|4N{bbByIcHD`w=8wuUYv3pzh=@vz1V11!mx-Qu`|`!)H#da~j%wUQ;ez zc&jw#W$Y@os{{d5B#=@XjmI@({x5hmA`Z#MA1nI%(VbGd0}mE`eRv}x4m9SAh?Cf^ zN%s1)N8d+sd_?D@x84l~8|`K>3SbiAPEX zy^31~jei%b2iH6)rG?muyEW^veb`*lo@$vmr8KE{ac9wWSeDLMS@+{h!`I>IhVdz- ztFTZGmGf}Xpy}_7h^GTMJc4LGtb-54RzZu|=j$vZK{uHmi#0000 + + + + + + diff --git a/src/android/static_resources/resources_oss/res/layout/candidate_view.xml b/src/android/static_resources/resources_oss/res/layout/candidate_view.xml index 51080afbe..35b7dd6a7 100644 --- a/src/android/static_resources/resources_oss/res/layout/candidate_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/candidate_view.xml @@ -30,8 +30,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - + - @@ -79,8 +79,7 @@ android:id="@+id/candidate_scroll_guide_view" android:layout_width="@dimen/candidate_scrollbar_width" android:layout_height="fill_parent" - android:layout_weight="0" - android:background="@drawable/candidate__slidebar_background" /> + android:layout_weight="0" /> diff --git a/src/android/static_resources/resources_oss/res/layout/first_time_launch.xml b/src/android/static_resources/resources_oss/res/layout/first_time_launch.xml index ee64b8403..9cff310ef 100644 --- a/src/android/static_resources/resources_oss/res/layout/first_time_launch.xml +++ b/src/android/static_resources/resources_oss/res/layout/first_time_launch.xml @@ -31,8 +31,7 @@ --> + android:layout_height="wrap_content" > - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/src/android/static_resources/resources_oss/res/layout/mozc_view.xml b/src/android/static_resources/resources_oss/res/layout/mozc_view.xml index 2d780b5ed..2825438b3 100644 --- a/src/android/static_resources/resources_oss/res/layout/mozc_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/mozc_view.xml @@ -30,7 +30,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - - + - - - - + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:gravity="bottom" + android:orientation="vertical" > + + android:layout_height="0dip" + android:layout_weight="1" /> - + android:layout_weight="0" > - + - - - - + + + + + android:layout_weight="1" + android:gravity="bottom" + android:orientation="vertical" > - + android:gravity="bottom" > - - - + android:layout_height="fill_parent" + android:layout_gravity="bottom" + android:baselineAligned="false" + android:gravity="bottom" + android:orientation="vertical" > - + - + + + + + + + - + android:layout_height="@dimen/narrow_frame_height" + android:layout_gravity="bottom" + android:orientation="vertical" + android:visibility="gone" > + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + android:clickable="true" + android:visibility="gone" + android:soundEffectsEnabled="false" /> + + + + + + + + + - - - diff --git a/src/android/static_resources/resources_oss/res/layout/right_frame.xml b/src/android/static_resources/resources_oss/res/layout/right_frame.xml index 2ba07eb47..55931ac07 100644 --- a/src/android/static_resources/resources_oss/res/layout/right_frame.xml +++ b/src/android/static_resources/resources_oss/res/layout/right_frame.xml @@ -31,66 +31,15 @@ --> - - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/src/android/static_resources/resources_oss/res/layout/symbol_candidate_view.xml b/src/android/static_resources/resources_oss/res/layout/symbol_candidate_view.xml index a45a8fe43..9f7718e45 100644 --- a/src/android/static_resources/resources_oss/res/layout/symbol_candidate_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/symbol_candidate_view.xml @@ -40,7 +40,6 @@ android:orientation="horizontal" > + android:layout_weight="0" /> @@ -69,7 +67,6 @@ android:layout_height="fill_parent" android:gravity="center" android:text="@string/symbol_input_no_history" - android:textColor="@android:color/black" android:textSize="@dimen/symbol_view_no_history_text_size" /> - - \ No newline at end of file diff --git a/src/android/static_resources/resources_oss/res/layout/symbol_view.xml b/src/android/static_resources/resources_oss/res/layout/symbol_view.xml index 5b5c7287e..9e26336be 100644 --- a/src/android/static_resources/resources_oss/res/layout/symbol_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/symbol_view.xml @@ -30,7 +30,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - @@ -54,18 +56,60 @@ + android:layout_height="fill_parent" > - + android:layout_height="fill_parent" + android:orientation="vertical" > + + + + + + + + + + + + + + + android:soundEffectsEnabled="false" > - + + + + + + + - - - + android:layout_height="0dip" + android:layout_weight="1" > - + - + + + + + + + + + + + + + + @@ -114,6 +197,12 @@ android:textColor="@android:color/white" android:textSize="20dip" android:visibility="gone" /> + + + android:paddingTop="2dip" + android:soundEffectsEnabled="false" > - + android:scaleType="fitCenter" + android:soundEffectsEnabled="false" + mozc:rawSrc="@raw/symbol__function__close" + mozc:maxImageWidth="@dimen/symbol_close_button_width" /> - + - + android:layout_weight="16.55" + android:contentDescription="@string/cd_symbol_window_number" + android:padding="0dip" + android:scaleType="fitCenter" + android:soundEffectsEnabled="false" + mozc:maxImageHeight="@dimen/symbol_major_number_height" /> - - - - - - - - + + + + + + + + + android:layout_weight="17.3" + android:contentDescription="@string/cd_symbol_window_enter" + android:focusable="true" + android:scaleType="fitCenter" + mozc:rawSrc="@raw/function__enter__icon" + mozc:maxImageWidth="@dimen/keyboard_enter_width" /> - \ No newline at end of file + diff --git a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_action_bar_view.xml b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_action_bar_view.xml deleted file mode 100644 index 47db29a8e..000000000 --- a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_action_bar_view.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_dictionary_name_dialog_view.xml b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_dictionary_name_dialog_view.xml index 6a7728003..d73ae876c 100644 --- a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_dictionary_name_dialog_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_dictionary_name_dialog_view.xml @@ -35,7 +35,6 @@ android:orientation="vertical" > - - - - - - - - - - diff --git a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_view.xml b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_view.xml index 9d4367f0a..709cc0ec3 100644 --- a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_view.xml @@ -45,43 +45,4 @@ android:layout_height="fill_parent" android:layout_below="@+id/user_dictionary_tool_dictionary_name_spinner" android:layout_above="@+id/user_dictionary_tool_split_action_bar" /> - - - - - - - - - - - - diff --git a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_word_register_dialog_view.xml b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_word_register_dialog_view.xml index a0c77b55e..30f51ee7a 100644 --- a/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_word_register_dialog_view.xml +++ b/src/android/static_resources/resources_oss/res/layout/user_dictionary_tool_word_register_dialog_view.xml @@ -39,7 +39,6 @@ android:paddingTop="9dip" > + android:showAsAction="never" + android:enabled="@bool/enable_user_dictionary_export" + android:visible="@bool/enable_user_dictionary_export" /> + + + + + 186.3dip + 226.3dip + + + 226.3dip + + 46.575dip + + 139.725dip + + diff --git a/src/android/static_resources/resources_oss/res/drawable/dropshadow_top.xml b/src/android/static_resources/resources_oss/res/values-h500dp-land/config.xml similarity index 87% rename from src/android/static_resources/resources_oss/res/drawable/dropshadow_top.xml rename to src/android/static_resources/resources_oss/res/values-h500dp-land/config.xml index 4a4cc8002..d791bebfa 100644 --- a/src/android/static_resources/resources_oss/res/drawable/dropshadow_top.xml +++ b/src/android/static_resources/resources_oss/res/values-h500dp-land/config.xml @@ -29,8 +29,9 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - - - + + + + 696845 + + diff --git a/src/android/static_resources/resources_oss/res/values-h500dp-land/dimens.xml b/src/android/static_resources/resources_oss/res/values-h500dp-land/dimens.xml new file mode 100644 index 000000000..e8bac4ef3 --- /dev/null +++ b/src/android/static_resources/resources_oss/res/values-h500dp-land/dimens.xml @@ -0,0 +1,101 @@ + + + + + + 150dip + + 75dip + 75dip + 75dip + 26dip + 14dip + 12dip + 12dip + 20dip + 10dip + 29dip + + + 63.75dip + + + 305dip + 255dip + + 350dip + + 32dip + 25dip + 28dip + 40dip + + + + 221dip + 46dip + + 305dip + + 191.25dip + + 50dip + + + 47dip + 75dip + 43dip + + + 39dip + 18dip + 23dip + 20dip + 16dip + 20dip + 23dip + 18dip + + 100dip + 145dip + 100dip + 18dip + + diff --git a/src/android/static_resources/resources_oss/res/drawable/dropshadow_left.xml b/src/android/static_resources/resources_oss/res/values-h570dp-land/config.xml similarity index 87% rename from src/android/static_resources/resources_oss/res/drawable/dropshadow_left.xml rename to src/android/static_resources/resources_oss/res/values-h570dp-land/config.xml index 39f01ee81..97186d854 100644 --- a/src/android/static_resources/resources_oss/res/drawable/dropshadow_left.xml +++ b/src/android/static_resources/resources_oss/res/values-h570dp-land/config.xml @@ -29,8 +29,8 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - - - + + + 9 + + diff --git a/src/android/static_resources/resources_oss/res/values-h570dp-land/dimens.xml b/src/android/static_resources/resources_oss/res/values-h570dp-land/dimens.xml new file mode 100644 index 000000000..75a14e717 --- /dev/null +++ b/src/android/static_resources/resources_oss/res/values-h570dp-land/dimens.xml @@ -0,0 +1,70 @@ + + + + + + + 68.25dip + + + 323dip + 273dip + + + 323dip + + 204.75dip + + 50dip + + + 47dip + 75dip + 51dip + + + 41dip + 19dip + 24dip + 20dip + 17dip + 20dip + 24dip + 19dip + + diff --git a/src/android/static_resources/resources_oss/res/values-h570dp-land/raws.xml b/src/android/static_resources/resources_oss/res/values-h570dp-land/raws.xml new file mode 100644 index 000000000..a22c5facc --- /dev/null +++ b/src/android/static_resources/resources_oss/res/values-h570dp-land/raws.xml @@ -0,0 +1,150 @@ + + + + + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + @null + + diff --git a/src/android/static_resources/resources_oss/res/values-h650dp-port/dimens.xml b/src/android/static_resources/resources_oss/res/values-h650dp-port/dimens.xml new file mode 100644 index 000000000..0e0f3df2a --- /dev/null +++ b/src/android/static_resources/resources_oss/res/values-h650dp-port/dimens.xml @@ -0,0 +1,54 @@ + + + + + + + 63.775dip + + + 335.1dip + 255.1dip + + + 295.1dip + + 191.325dip + + diff --git a/src/android/static_resources/resources_oss/res/values/styles.xml b/src/android/static_resources/resources_oss/res/values-h720dp-land/dimens.xml similarity index 62% rename from src/android/static_resources/resources_oss/res/values/styles.xml rename to src/android/static_resources/resources_oss/res/values-h720dp-land/dimens.xml index e87f661af..cf5ae4baa 100644 --- a/src/android/static_resources/resources_oss/res/values/styles.xml +++ b/src/android/static_resources/resources_oss/res/values-h720dp-land/dimens.xml @@ -29,25 +29,25 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> + - - - - - + + 86.25dip + + + 396.5dip + 346.5dip + + + 396.5dip + + 259.875dip - -