blob: 16f74db16760daa30c14d46fecef7f214bde3b98 [file] [log] [blame]
// 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;
}
}