| // 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.FeedbackManager.FeedbackEvent; |
| 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; |
| 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.resources.R; |
| 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.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; |
| import android.graphics.Rect; |
| import android.inputmethodservice.InputMethodService; |
| 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; |
| import android.view.View; |
| 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; |
| import java.util.List; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Manages Input, Candidate and Extracted views. |
| * |
| */ |
| 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); |
| } |
| |
| @Override |
| public void onConversionCandidateSelected(int candidateId, Optional<Integer> rowIndex) { |
| // Restore the keyboard frame if hidden. |
| if (mozcView != null) { |
| mozcView.resetKeyboardFrameVisibility(); |
| } |
| super.onConversionCandidateSelected(candidateId, rowIndex); |
| } |
| } |
| |
| /** |
| * Converts S/W Keyboard's keycode to KeyEvent instance. |
| */ |
| void onKey(int primaryCode, List<TouchEvent> touchEventList) { |
| if (primaryCode == keycodeCapslock || primaryCode == keycodeAlt) { |
| // Ignore those key events because they are handled by KeyboardView, |
| // but send touchEventList for logging usage stats. |
| eventListener.onKeyEvent(null, null, null, touchEventList); |
| return; |
| } |
| |
| // Keyboard switch event. |
| 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 == keycodeChartypeToAbc123) { |
| japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.ALPHABET_NUMBER); |
| } |
| propagateSoftwareKeyboardChange(touchEventList); |
| return; |
| } |
| |
| if (primaryCode == keycodeGlobe) { |
| imeSwitcher.switchToNextInputMethod(false); |
| return; |
| } |
| |
| if (primaryCode == keycodeMenuDialog || primaryCode == keycodeImePickerDialog) { |
| // We need to reset the keyboard, otherwise it would miss the ACTION_UP event. |
| if (mozcView != null) { |
| mozcView.resetKeyboardViewState(); |
| } |
| eventListener.onShowMenuDialog(touchEventList); |
| if (primaryCode == keycodeMenuDialog) { |
| showMenuDialog(); |
| } else if (primaryCode == keycodeImePickerDialog) { |
| showImePickerDialog(); |
| } |
| return; |
| } |
| |
| if (primaryCode == keycodeSymbol) { |
| if (mozcView != null) { |
| mozcView.showSymbolInputView(Optional.<SymbolMajorCategory>absent()); |
| } |
| return; |
| } |
| |
| if (primaryCode == keycodeSymbolEmoji) { |
| if (mozcView != null) { |
| mozcView.showSymbolInputView(Optional.of(SymbolMajorCategory.EMOJI)); |
| } |
| return; |
| } |
| |
| if (primaryCode == keycodeUndo) { |
| eventListener.onUndo(touchEventList); |
| return; |
| } |
| |
| Optional<ProtoCommands.KeyEvent> mozcKeyEvent = |
| primaryKeyCodeConverter.createMozcKeyEvent(primaryCode, touchEventList); |
| eventListener.onKeyEvent(mozcKeyEvent.orNull(), |
| primaryKeyCodeConverter.getPrimaryCodeKeyEvent(primaryCode), |
| getActiveSoftwareKeyboardModel().getKeyboardSpecification(), |
| touchEventList); |
| } |
| |
| /** |
| * A simple KeyboardActionListener implementation which just delegates onKey event to |
| * ViewManager's onKey method. |
| */ |
| class KeyboardActionAdapter implements KeyboardActionListener { |
| @Override |
| public void onCancel() { |
| } |
| |
| @Override |
| public void onKey(int primaryCode, List<TouchEvent> touchEventList) { |
| ViewManager.this.onKey(primaryCode, touchEventList); |
| } |
| |
| @Override |
| public void onPress(int primaryCode) { |
| if (primaryCode != KeyEntity.INVALID_KEY_CODE) { |
| eventListener.onFireFeedbackEvent(FeedbackEvent.KEY_DOWN); |
| } |
| } |
| |
| @Override |
| 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; |
| |
| // The view of the MechaMozc. |
| @VisibleForTesting MozcView mozcView; |
| |
| // Menu dialog and its listener. |
| private final MenuDialogListener menuDialogListener; |
| @VisibleForTesting MenuDialog menuDialog = null; |
| |
| // IME switcher instance to detect that voice input is available or not. |
| private final ImeSwitcher imeSwitcher; |
| |
| /** Key event handler to handle events on Mozc server. */ |
| private final KeyEventHandler keyEventHandler; |
| |
| /** 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; |
| |
| /** 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 */ |
| private boolean fullscreenMode = false; |
| |
| /** Current narrow mode */ |
| private boolean narrowMode = false; |
| |
| /** Current popup enabled state. */ |
| private boolean popupEnabled = true; |
| |
| /** Current Globe button enabled state. */ |
| private boolean globeButtonEnabled = false; |
| |
| /** True if CursorAnchorInfo is enabled. */ |
| private boolean cursorAnchroInfoEnabled = false; |
| |
| /** True if hardware keyboard exists. */ |
| private boolean hardwareKeyboardExist = false; |
| |
| /** |
| * True if voice input is eligible. |
| * <p> |
| * This conditions is calculated based on following conditions. |
| * <ul> |
| * <li>VoiceIME's status: If VoiceIME is not available, this flag becomes false. |
| * <li>EditorInfo: If current editor does not want to use voice input, this flag becomes false. |
| * <ul> |
| * <li>Voice input might be explicitly forbidden by the editor. |
| * <li>Voice input should be useless for the number input editors. |
| * <li>Voice input should be useless for password field. |
| * <ul> |
| * </ul> |
| */ |
| private boolean isVoiceInputEligible = false; |
| |
| private boolean isVoiceInputEnabledByPreference = true; |
| |
| private int flickSensitivity = 0; |
| |
| @VisibleForTesting EmojiProviderType emojiProviderType = EmojiProviderType.NONE; |
| |
| /** Current skin type. */ |
| private Skin skin = Skin.getFallbackInstance(); |
| |
| private LayoutAdjustment layoutAdjustment = LayoutAdjustment.FILL; |
| |
| /** Percentage of keyboard height */ |
| private int keyboardHeightRatio = 100; |
| |
| // Keycodes defined in resource files. |
| // Printable keys are not defined. Refer them using character literal. |
| // These are "constant values" as a matter of practice, |
| // but such name like "KEYCODE_LEFT" makes Lint unhappy |
| // because they are not "static final". |
| private final int keycodeChartypeToKana; |
| private final int keycodeChartypeToAbc; |
| 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. */ |
| private final KeyboardActionAdapter keyboardActionListener; |
| |
| private final PrimaryKeyCodeConverter primaryKeyCodeConverter; |
| |
| public ViewManager(Context context, ViewEventListener listener, |
| SymbolHistoryStorage symbolHistoryStorage, ImeSwitcher imeSwitcher, |
| MenuDialogListener menuDialogListener) { |
| this(context, listener, symbolHistoryStorage, imeSwitcher, menuDialogListener, |
| new ProbableKeyEventGuesser(context.getAssets()), new HardwareKeyboard()); |
| } |
| |
| @VisibleForTesting |
| ViewManager(Context context, ViewEventListener listener, |
| SymbolHistoryStorage symbolHistoryStorage, ImeSwitcher imeSwitcher, |
| @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); |
| keycodeChartypeToAbc = res.getInteger(R.integer.key_chartype_to_abc); |
| 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); |
| keycodeMenuDialog = res.getInteger(R.integer.key_menu_dialog); |
| keycodeImePickerDialog = res.getInteger(R.integer.key_ime_picker_dialog); |
| |
| // Inject some logics into the listener. |
| eventListener = new ViewManagerEventListener(listener); |
| keyboardActionListener = new KeyboardActionAdapter(); |
| // Prepare callback object. |
| keyEventHandler = new KeyEventHandler( |
| Looper.getMainLooper(), |
| keyboardActionListener, |
| res.getInteger(R.integer.config_repeat_key_delay), |
| res.getInteger(R.integer.config_repeat_key_interval), |
| res.getInteger(R.integer.config_long_press_key_delay)); |
| |
| this.imeSwitcher = imeSwitcher; |
| this.menuDialogListener = menuDialogListener; |
| this.symbolCandidateStorage = new SymbolCandidateStorage(symbolHistoryStorage); |
| this.hardwareKeyboard = hardwareKeyboard; |
| } |
| |
| /** |
| * Creates new input view. |
| * |
| * "Input view" is a software keyboard in almost all cases. |
| * |
| * Previously created input view is not accessed any more after calling this method. |
| * |
| * @param context |
| * @return newly created view. |
| */ |
| @Override |
| public MozcView createMozcView(Context context) { |
| // Because an issue about native bitmap memory management on older Android, |
| // there is a potential OutOfMemoryError. To reduce such an error case, |
| // we retry to inflate or to create drawable when OOM is found. |
| // Here is the injecting point of the procedure. |
| LayoutInflater inflater = LayoutInflater.from(context); |
| inflater = inflater.cloneInContext(MozcUtil.getContextWithOutOfMemoryRetrial(context)); |
| mozcView = MozcUtil.inflateWithOutOfMemoryRetrial( |
| MozcView.class, inflater, R.layout.mozc_view, Optional.<ViewGroup>absent(), false); |
| // Suppress update of View's internal state |
| // 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, |
| 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. |
| leftAdjustButtonClickListener, |
| rightAdjustButtonClickListener, |
| microphoneButtonClickListener); |
| |
| mozcView.setKeyEventHandler(keyEventHandler); |
| |
| propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList()); |
| mozcView.setFullscreenMode(fullscreenMode); |
| mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode); |
| // At the moment, it is necessary to set the storage to the view, *before* setting emoji |
| // provider type. |
| // TODO(hidehiko): Remove the restriction. |
| mozcView.setSymbolCandidateStorage(symbolCandidateStorage); |
| mozcView.setEmojiProviderType(emojiProviderType); |
| mozcView.setPopupEnabled(popupEnabled); |
| mozcView.setFlickSensitivity(flickSensitivity); |
| mozcView.setSkin(skin); |
| |
| // Clear the menu dialog. |
| menuDialog = null; |
| |
| reset(); |
| |
| mozcView.setVisibility(View.VISIBLE); |
| return mozcView; |
| } |
| |
| private void showMenuDialog() { |
| if (mozcView == null) { |
| MozcLog.w("mozcView is not initialized."); |
| return; |
| } |
| |
| menuDialog = new MenuDialog(mozcView.getContext(), Optional.fromNullable(menuDialogListener)); |
| IBinder windowToken = mozcView.getWindowToken(); |
| if (windowToken == null) { |
| MozcLog.w("Unknown window token"); |
| } else { |
| menuDialog.setWindowToken(windowToken); |
| } |
| menuDialog.show(); |
| } |
| |
| private void showImePickerDialog() { |
| if (mozcView == null) { |
| MozcLog.w("mozcView is not initialized."); |
| return; |
| } |
| if (!MozcUtil.requestShowInputMethodPicker(mozcView.getContext())) { |
| MozcLog.e("Failed to send message to launch the input method picker dialog."); |
| } |
| } |
| |
| private void maybeDismissMenuDialog() { |
| MenuDialog menuDialog = this.menuDialog; |
| if (menuDialog != null) { |
| menuDialog.dismiss(); |
| } |
| } |
| |
| /** |
| * Renders views which this instance own based on Command.Output. |
| * |
| * Note that showing/hiding views is Service's responsibility. |
| */ |
| @Override |
| public void render(Command outCommand) { |
| if (outCommand == null) { |
| return; |
| } |
| if (mozcView == null) { |
| return; |
| } |
| 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, |
| // we skip the following rendering. |
| return; |
| } |
| |
| mozcView.setCommand(outCommand); |
| if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() == 0) { |
| // If the candidate is empty (i.e. the CandidateView will go to GONE), |
| // reset the keyboard so that a user can type keyboard. |
| mozcView.resetKeyboardFrameVisibility(); |
| } |
| } |
| |
| /** |
| * @return the current keyboard specification. |
| */ |
| @Override |
| public KeyboardSpecification getKeyboardSpecification() { |
| return getActiveSoftwareKeyboardModel().getKeyboardSpecification(); |
| } |
| |
| /** Set {@code EditorInfo} instance to the current view. */ |
| @Override |
| public void setEditorInfo(EditorInfo 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); |
| // TODO(hsumita): Set input type on Hardware keyboard, too. Otherwise, Hiragana input can be |
| // enabled unexpectedly. (e.g. Number text field.) |
| propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList()); |
| } |
| |
| private boolean shouldVoiceImeBeEnabled() { |
| // Disable voice IME if hardware keyboard exists to avoid a framework bug. |
| return isVoiceInputEligible && isVoiceInputEnabledByPreference && !hardwareKeyboardExist |
| && imeSwitcher.isVoiceImeAvailable(); |
| } |
| |
| @Override |
| public void setTextForActionButton(CharSequence text) { |
| // TODO(mozc-team): Implement action button handling. |
| } |
| |
| @Override |
| public boolean hideSubInputView() { |
| if (mozcView == null) { |
| return false; |
| } |
| 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; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Creates and sets a keyboard represented by the resource id to the input frame. |
| * <p> |
| * Note that this method requires inputFrameView is not null, and its first child is |
| * the JapaneseKeyboardView. |
| */ |
| 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<TouchEvent> 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); |
| |
| // 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.<TouchEvent>emptyList()); |
| } else { |
| eventListener.onKeyEvent( |
| hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event), |
| specification, Collections.<TouchEvent>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(); |
| } |
| |
| /** |
| * Set this keyboard layout to the specified one. |
| * @param keyboardLayout New keyboard layout. |
| * @throws NullPointerException If <code>keyboardLayout</code> is <code>null</code>. |
| */ |
| @Override |
| public void setKeyboardLayout(KeyboardLayout keyboardLayout) { |
| Preconditions.checkNotNull(keyboardLayout); |
| |
| if (japaneseSoftwareKeyboardModel.getKeyboardLayout() != keyboardLayout) { |
| // If changed, clear the keyboard cache. |
| keyboardFactory.clear(); |
| } |
| |
| japaneseSoftwareKeyboardModel.setKeyboardLayout(keyboardLayout); |
| propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList()); |
| } |
| |
| /** |
| * Set the input style. |
| * @param inputStyle new input style. |
| * @throws NullPointerException If <code>inputStyle</code> is <code>null</code>. |
| * TODO(hidehiko): Refactor out following keyboard switching logic into another class. |
| */ |
| @Override |
| public void setInputStyle(InputStyle inputStyle) { |
| Preconditions.checkNotNull(inputStyle); |
| |
| if (japaneseSoftwareKeyboardModel.getInputStyle() != inputStyle) { |
| // If changed, clear the keyboard cache. |
| keyboardFactory.clear(); |
| } |
| |
| japaneseSoftwareKeyboardModel.setInputStyle(inputStyle); |
| propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList()); |
| } |
| |
| @Override |
| public void setQwertyLayoutForAlphabet(boolean qwertyLayoutForAlphabet) { |
| if (japaneseSoftwareKeyboardModel.isQwertyLayoutForAlphabet() != qwertyLayoutForAlphabet) { |
| // If changed, clear the keyboard cache. |
| keyboardFactory.clear(); |
| } |
| |
| japaneseSoftwareKeyboardModel.setQwertyLayoutForAlphabet(qwertyLayoutForAlphabet); |
| propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList()); |
| } |
| |
| @Override |
| public void setFullscreenMode(boolean fullscreenMode) { |
| this.fullscreenMode = fullscreenMode; |
| if (mozcView != null) { |
| mozcView.setFullscreenMode(fullscreenMode); |
| } |
| } |
| |
| @Override |
| public boolean isFullscreenMode() { |
| return fullscreenMode; |
| } |
| |
| @Override |
| public void setFlickSensitivity(int flickSensitivity) { |
| this.flickSensitivity = flickSensitivity; |
| if (mozcView != null) { |
| mozcView.setFlickSensitivity(flickSensitivity); |
| } |
| } |
| |
| @Override |
| public void setEmojiProviderType(EmojiProviderType emojiProviderType) { |
| Preconditions.checkNotNull(emojiProviderType); |
| |
| this.emojiProviderType = emojiProviderType; |
| if (mozcView != null) { |
| mozcView.setEmojiProviderType(emojiProviderType); |
| } |
| } |
| |
| /** |
| * 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 newNarrowMode) { |
| boolean previousNarrowMode = this.narrowMode; |
| this.narrowMode = newNarrowMode; |
| if (mozcView != null) { |
| mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, newNarrowMode); |
| } |
| updateMicrophoneButtonEnabled(); |
| if (previousNarrowMode != newNarrowMode) { |
| eventListener.onNarrowModeChanged(newNarrowMode); |
| } |
| } |
| |
| /** |
| * Returns true if we should transit to narrow mode, |
| * based on returned {@code Command} and {@code KeyEventInterface} from the server. |
| * |
| * <p>If all of the following conditions are satisfied, narrow mode is shown. |
| * <ul> |
| * <li>The key event is from h/w keyboard. |
| * <li>The key event has printable character without modifier. |
| * </ul> |
| */ |
| @Override |
| public void maybeTransitToNarrowMode(Command command, KeyEventInterface keyEventInterface) { |
| Preconditions.checkNotNull(command); |
| // 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().isPresent()) { |
| return; |
| } |
| // 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); |
| } |
| |
| @Override |
| public boolean isNarrowMode() { |
| return narrowMode; |
| } |
| |
| @Override |
| public boolean isFloatingCandidateMode() { |
| return mozcView != null && mozcView.isFloatingCandidateMode(); |
| } |
| |
| @Override |
| public void setPopupEnabled(boolean popupEnabled) { |
| this.popupEnabled = popupEnabled; |
| if (mozcView != null) { |
| mozcView.setPopupEnabled(popupEnabled); |
| } |
| } |
| |
| @Override |
| 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 setHardwareKeyMap(HardwareKeyMap hardwareKeyMap) { |
| hardwareKeyboard.setHardwareKeyMap(Preconditions.checkNotNull(hardwareKeyMap)); |
| } |
| |
| @Override |
| public void setSkin(Skin skin) { |
| this.skin = Preconditions.checkNotNull(skin); |
| if (mozcView != null) { |
| mozcView.setSkin(skin); |
| } |
| } |
| |
| @Override |
| public void setMicrophoneButtonEnabledByPreference(boolean microphoneButtonEnabled) { |
| this.isVoiceInputEnabledByPreference = microphoneButtonEnabled; |
| updateMicrophoneButtonEnabled(); |
| } |
| |
| /** |
| * Set layout adjustment and show animation if required. |
| * <p> |
| * 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 |
| public void setKeyboardHeightRatio(int keyboardHeightRatio) { |
| this.keyboardHeightRatio = keyboardHeightRatio; |
| if (mozcView != null) { |
| mozcView.setKeyboardHeightRatio(keyboardHeightRatio); |
| } |
| } |
| |
| /** |
| * Reset the status of the current input view. |
| * |
| * This method must be called when the IME is turned on. |
| * Note that this method can be called before {@link #createMozcView(Context)} |
| * so null-check is mandatory. |
| */ |
| @Override |
| public void reset() { |
| if (mozcView != null) { |
| mozcView.reset(); |
| } |
| |
| viewLayerKeyEventHandler.reset(); |
| |
| // Reset menu dialog. |
| maybeDismissMenuDialog(); |
| } |
| |
| @Override |
| public void computeInsets(Context context, InputMethodService.Insets outInsets, Window window) { |
| // The IME's area is prioritized than app's. |
| // - contentTopInsets |
| // - This is the top part of the UI that is the main content. |
| // - This affects window layout. |
| // So if this value is changed, resizing the application behind happens. |
| // - This value is relative to the top edge of the input method window. |
| // - visibleTopInsets |
| // - This is the top part of the UI that is visibly covering the application behind it. |
| // Changing this value will not cause resizing the application. |
| // - This is *not* to clip IME's drawing area. |
| // - This value is relative to the top edge of the input method window. |
| // Thus it seems that we have to guarantee contentTopInsets <= visibleTopInsets. |
| // If contentTopInsets < visibleTopInsets, the app's UI is drawn on IME's area |
| // but almost all (or completely all?) application does not draw anything |
| // on such "outside" area from the app's window. |
| // Conclusion is that we should guarantee contentTopInsets == visibleTopInsets. |
| // |
| // On Honeycomb or later version, we cannot take touch events outside of IME window. |
| // As its workaround, we cover the almost screen by transparent view, and we need to consider |
| // the gap between the top of user visible IME window, and the transparent view's top here. |
| // Note that touch events for the transparent view will be ignored by IME and automatically |
| // sent to the application if it is not for the IME. |
| |
| View contentView = window.findViewById(Window.ID_ANDROID_CONTENT); |
| int contentViewWidth = contentView.getWidth(); |
| int contentViewHeight = contentView.getHeight(); |
| |
| if (mozcView == null) { |
| outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_CONTENT; |
| outInsets.contentTopInsets = contentViewHeight |
| - context.getResources().getDimensionPixelSize(R.dimen.input_frame_height); |
| outInsets.visibleTopInsets = outInsets.contentTopInsets; |
| return; |
| } |
| |
| mozcView.setInsets(contentViewWidth, contentViewHeight, outInsets); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| primaryKeyCodeConverter.setConfiguration(newConfig); |
| hardwareKeyboardExist = newConfig.keyboard != Configuration.KEYBOARD_NOKEYS; |
| } |
| |
| @Override |
| public boolean isKeyConsumedOnViewAsynchronously(KeyEvent event) { |
| return viewLayerKeyEventHandler.evaluateKeyEvent(Preconditions.checkNotNull(event)); |
| } |
| |
| @Override |
| public void consumeKeyOnViewSynchronously(KeyEvent 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.<TouchEvent>emptyList()); |
| } |
| } |
| |
| @Override |
| public boolean isGenericMotionToConsume(MotionEvent event) { |
| return false; |
| } |
| |
| @Override |
| public boolean consumeGenericMotion(MotionEvent event) { |
| return false; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public ViewEventListener getEventListener() { |
| return eventListener; |
| } |
| |
| /** |
| * Returns active (shown) JapaneseSoftwareKeyboardModel. |
| * If symbol picker is shown, symbol-number keyboard's is returned. |
| */ |
| @VisibleForTesting |
| @Override |
| public JapaneseSoftwareKeyboardModel getActiveSoftwareKeyboardModel() { |
| if (isSymbolInputViewVisible) { |
| return symbolNumberSoftwareKeyboardModel; |
| } else { |
| return japaneseSoftwareKeyboardModel; |
| } |
| } |
| |
| @VisibleForTesting |
| @Override |
| public boolean isPopupEnabled() { |
| return popupEnabled; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public int getFlickSensitivity() { |
| return flickSensitivity; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public EmojiProviderType getEmojiProviderType() { |
| return emojiProviderType; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public Skin getSkin() { |
| return skin; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public boolean isMicrophoneButtonEnabledByPreference() { |
| return isVoiceInputEnabledByPreference; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public LayoutAdjustment getLayoutAdjustment() { |
| return layoutAdjustment; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public int getKeyboardHeightRatio() { |
| return keyboardHeightRatio; |
| } |
| |
| @VisibleForTesting |
| @Override |
| public HardwareKeyMap getHardwareKeyMap() { |
| return hardwareKeyboard.getHardwareKeyMap(); |
| } |
| |
| @Override |
| public void trimMemory() { |
| if (mozcView != null) { |
| mozcView.trimMemory(); |
| } |
| } |
| |
| @Override |
| 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; |
| } |
| } |