| // Copyright 2010-2015, 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.DependencyFactory.Dependency; |
| import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface; |
| 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.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.protobuf.ProtoCommands.Input.TouchEvent; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment; |
| import com.google.common.base.Optional; |
| |
| import android.content.res.Configuration; |
| import android.test.InstrumentationTestCase; |
| import android.view.InputDevice; |
| import android.view.KeyCharacterMap; |
| import android.view.KeyEvent; |
| |
| import java.util.Collections; |
| |
| /** |
| * The focus of this test is to check the event flow from h/w key event, |
| * MozcService, (real) SessionExecutor to MozcService. |
| * Key character conversion is not the focus. |
| * |
| * <p>To write a scenario easily this test introduces DSL like format. |
| */ |
| public class KeyEventScenarioTest extends InstrumentationTestCase { |
| |
| private StubMozcService service; |
| |
| @Override |
| public void setUp() { |
| service = null; |
| DependencyFactory.setDependency(Optional.<Dependency>absent()); |
| } |
| |
| private void verifyNarrowMode(boolean expectation) { |
| waitForExecution(); |
| assertEquals(expectation, service.viewManager.isNarrowMode()); |
| } |
| |
| private void verifyKeyboardSpecificationInView(KeyboardSpecification specification) { |
| waitForExecution(); |
| assertEquals(specification, service.viewManager.getKeyboardSpecification()); |
| } |
| |
| private void verifyKeyboardSpecificationInService(KeyboardSpecification specification) { |
| waitForExecution(); |
| assertEquals(specification, service.currentKeyboardSpecification); |
| } |
| |
| private void verifyCompositionText(String expectation) { |
| waitForExecution(); |
| Output output = service.lastOutput; |
| String preeditText; |
| if (!output.hasPreedit()) { |
| preeditText = ""; |
| } else { |
| Preedit preedit = output.getPreedit(); |
| StringBuilder builder = new StringBuilder(); |
| for (Segment segment : preedit.getSegmentList()) { |
| builder.append(segment.getValue()); |
| } |
| preeditText = builder.toString(); |
| } |
| assertEquals(expectation, preeditText); |
| } |
| |
| private void verifySubmittedText(String expectation) { |
| waitForExecution(); |
| Output output = service.lastOutput; |
| String actual; |
| if (!output.hasResult() || !output.getResult().hasValue()) { |
| actual = ""; |
| } else { |
| actual = output.getResult().getValue(); |
| } |
| assertEquals(expectation, actual); |
| } |
| |
| private void verifyCompositionMode(CompositionMode expected) { |
| waitForExecution(); |
| Output output = service.lastOutput; |
| if (!output.hasMode()) { |
| throw new IllegalStateException("No composition mode."); |
| } |
| assertEquals(expected, output.getMode()); |
| } |
| |
| private void hardwareKeyEvent(int keyCode, int metaState, int scanCode, int source) { |
| KeyEvent keyEvent = new KeyEvent(0, 0, 0, keyCode, 0, metaState, |
| KeyCharacterMap.VIRTUAL_KEYBOARD, scanCode); |
| keyEvent.setSource(source); |
| service.onKeyDownInternal(keyEvent.getKeyCode(), keyEvent, |
| getDefaultDeviceConfiguration()); |
| service.onKeyUp(keyEvent.getKeyCode(), keyEvent); |
| } |
| |
| private void hardwareKeyEventSequence(String text) { |
| KeyCharacterMap keyCharacterMap = |
| KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); |
| KeyEvent[] events = keyCharacterMap.getEvents(text.toCharArray()); |
| for (KeyEvent event : events) { |
| event.setSource(InputDevice.SOURCE_KEYBOARD); |
| switch (event.getAction()) { |
| case KeyEvent.ACTION_DOWN: |
| service.onKeyDownInternal(event.getKeyCode(), event, |
| getDefaultDeviceConfiguration()); |
| break; |
| case KeyEvent.ACTION_UP: |
| service.onKeyUp(event.getKeyCode(), event); |
| break; |
| default: |
| throw new IllegalArgumentException("Only ACTION_DOWN/UP are allowed."); |
| } |
| } |
| } |
| |
| private void softwareKeyEvent(int mozcKeyCode) { |
| service.viewManager.getKeyboardActionListener().onKey(mozcKeyCode, |
| Collections.<TouchEvent>emptyList()); |
| } |
| |
| private void setHardwareKeyMap(HardwareKeyMap keyMap) { |
| service.viewManager.setHardwareKeyMap(keyMap); |
| } |
| |
| private void setKeyboardSpecification(KeyboardLayout layout, InputStyle inputStyle) { |
| service.viewManager.setKeyboardLayout(layout); |
| service.viewManager.setInputStyle(inputStyle); |
| } |
| |
| // Waits for all the execution of the session executor. |
| // Expected to be called from verify* methods. |
| private void waitForExecution() { |
| service.sessionExecutor.waitForAllQueuesForEmpty(); |
| } |
| |
| private static Configuration getDefaultDeviceConfiguration() { |
| Configuration configuration = new Configuration(); |
| configuration.orientation = Configuration.ORIENTATION_PORTRAIT; |
| // Note: Some other fields might be needed. |
| // But currently only orientation field causes flaky test results. |
| return configuration; |
| } |
| |
| class StubMozcService extends MozcService { |
| public Output lastOutput = Output.getDefaultInstance(); |
| @Override |
| public boolean isInputViewShown() { |
| // Always shown. |
| // Showing real view from test code is really hard so use this hack instead. |
| return true; |
| } |
| @Override |
| void renderInputConnection(Command command, KeyEventInterface keyEvent) { |
| // Capture the Output. |
| this.lastOutput = command.getOutput(); |
| } |
| @Override |
| public void onConfigurationChanged(Configuration unused) { |
| // Use static config for stable testing. |
| super.onConfigurationChanged(getDefaultDeviceConfiguration()); |
| } |
| @Override |
| Configuration getConfiguration() { |
| // Use static config for stable testing. |
| return getDefaultDeviceConfiguration(); |
| } |
| } |
| |
| private void createService(Dependency dependency) { |
| DependencyFactory.setDependency(Optional.of(dependency)); |
| service = new StubMozcService(); |
| service.attachBaseContext(getInstrumentation().getTargetContext()); |
| service.onCreate(); |
| } |
| |
| /** |
| * Normal case for Japanese keyboard. |
| */ |
| public void testJapaneseHardWareKeyEvent_NormalCase() { |
| createService(DependencyFactory.TOUCH_FRAGMENT_PREF); |
| setHardwareKeyMap(HardwareKeyMap.JAPANESE109A); |
| setKeyboardSpecification(KeyboardLayout.TWELVE_KEYS, InputStyle.TOGGLE_FLICK); |
| // Before typing, software keyboard is shown. |
| verifyNarrowMode(false); |
| // Keyboard specs are for software keyboard. |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| verifyKeyboardSpecificationInService(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| // TODO(matsuzakit): Make HardwareKeyEventSequence work for Japanese keyboard. |
| // If no scan code exists, current implementation doesn't work. |
| hardwareKeyEvent(KeyEvent.KEYCODE_A, 0, 0x001e, InputDevice.SOURCE_KEYBOARD); |
| // Once a character is typed from h/w keyboard, narrow mode should be shown. |
| verifyNarrowMode(true); |
| // View's spec (== s/w kb's spec) is kept. |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| // Service's spec is updated. |
| verifyKeyboardSpecificationInService(KeyboardSpecification.HARDWARE_QWERTY_KANA); |
| } |
| |
| /** |
| * The same scenario as testJapaneseHardWareKeyEvent_NormalCase for default keyboard. |
| */ |
| public void testDefaultHardWareKeyEvent_NormalCase() { |
| createService(DependencyFactory.TOUCH_FRAGMENT_PREF); |
| // Note: KeyEvent generated by HardwareKeyEventSequence doesn't have |
| // scancode so JAPANESE109 doesn't work. |
| setHardwareKeyMap(HardwareKeyMap.DEFAULT); |
| setKeyboardSpecification(KeyboardLayout.TWELVE_KEYS, InputStyle.TOGGLE_FLICK); |
| verifyNarrowMode(false); |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| verifyKeyboardSpecificationInService(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| hardwareKeyEventSequence("hello"); |
| verifyCompositionText("へっぉ"); |
| verifyNarrowMode(true); |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| verifyKeyboardSpecificationInService(KeyboardSpecification.HARDWARE_QWERTY_KANA); |
| hardwareKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, InputDevice.SOURCE_KEYBOARD); |
| verifyCompositionText(""); |
| verifySubmittedText("へっぉ"); |
| } |
| |
| /** |
| * Similar scenario as testJapaneseHardWareKeyEvent_NormalCase for software keyboard. |
| * |
| * <p>Narrow mode is not shown. |
| */ |
| public void testSoftwareKeyboard_NormalCase() { |
| createService(DependencyFactory.TOUCH_FRAGMENT_PREF); |
| setKeyboardSpecification(KeyboardLayout.TWELVE_KEYS, InputStyle.TOGGLE_FLICK); |
| verifyNarrowMode(false); |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| verifyKeyboardSpecificationInService(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| softwareKeyEvent('1'); |
| verifyCompositionText("あ"); |
| // No narrow mode. |
| verifyNarrowMode(false); |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| verifyKeyboardSpecificationInService(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| softwareKeyEvent('\n'); |
| verifyCompositionText(""); |
| verifySubmittedText("あ"); |
| } |
| |
| /** |
| * Even if h/w keyboard sends some special keys, transition to narrow mode happens. |
| */ |
| public void testHardwareSpecialKeyEvent_TransitionToNarrowMode() { |
| createService(DependencyFactory.TOUCH_FRAGMENT_PREF); |
| setHardwareKeyMap(HardwareKeyMap.DEFAULT); |
| setKeyboardSpecification(KeyboardLayout.TWELVE_KEYS, InputStyle.TOGGLE_FLICK); |
| verifyKeyboardSpecificationInView(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| verifyKeyboardSpecificationInService(KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA); |
| |
| verifyNarrowMode(false); |
| hardwareKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, InputDevice.SOURCE_KEYBOARD); |
| verifyNarrowMode(true); |
| |
| service.viewManager.setNarrowMode(false); |
| verifyNarrowMode(false); |
| hardwareKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 0, 0, InputDevice.SOURCE_KEYBOARD); |
| verifyNarrowMode(true); |
| |
| service.viewManager.setNarrowMode(false); |
| verifyNarrowMode(false); |
| hardwareKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 0, 0, InputDevice.SOURCE_KEYBOARD); |
| verifyNarrowMode(true); |
| |
| service.viewManager.setNarrowMode(false); |
| verifyNarrowMode(false); |
| hardwareKeyEvent( |
| KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_SHIFT_ON, 0, InputDevice.SOURCE_KEYBOARD); |
| verifyNarrowMode(true); |
| |
| service.viewManager.setNarrowMode(false); |
| verifyNarrowMode(false); |
| hardwareKeyEvent( |
| KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_SHIFT_ON, 0, InputDevice.SOURCE_KEYBOARD); |
| verifyNarrowMode(true); |
| |
| // Nothing is input. |
| verifyCompositionText(""); |
| } |
| |
| /** |
| * Zen/Han key (scan-code 0x29) changes composition mode. |
| */ |
| public void testJapaneseHardWareKeyEvent_ZenHanKey() { |
| createService(DependencyFactory.TOUCH_FRAGMENT_PREF); |
| setHardwareKeyMap(HardwareKeyMap.JAPANESE109A); |
| hardwareKeyEvent(KeyEvent.KEYCODE_GRAVE, 0, 0x29, InputDevice.SOURCE_KEYBOARD); |
| verifyCompositionMode(CompositionMode.HALF_ASCII); |
| // No characters (including grave) should be shown. |
| verifyCompositionText(""); |
| } |
| } |