| // Copyright 2010-2014, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| package org.mozc.android.inputmethod.japanese; |
| |
| import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent; |
| import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackListener; |
| 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.CompositionSwitchMode; |
| import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboardSpecification; |
| import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification; |
| import org.mozc.android.inputmethod.japanese.model.SelectionTracker; |
| import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage.SymbolHistoryStorage; |
| import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory; |
| import org.mozc.android.inputmethod.japanese.mushroom.MushroomResultProxy; |
| import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference; |
| import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Context.InputFieldType; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.DeletionRange; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.GenericStorageEntry.StorageType; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input; |
| 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 org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment.Annotation; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand.UsageStatsEvent; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoConfig.Config; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoConfig.Config.SelectionShortcut; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoConfig.Config.SessionKeymap; |
| import org.mozc.android.inputmethod.japanese.resources.R; |
| import org.mozc.android.inputmethod.japanese.session.SessionExecutor; |
| import org.mozc.android.inputmethod.japanese.session.SessionHandlerFactory; |
| import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory; |
| import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher; |
| import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| import com.google.protobuf.ByteString; |
| |
| import android.annotation.SuppressLint; |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.SharedPreferences.OnSharedPreferenceChangeListener; |
| import android.content.res.Configuration; |
| import android.inputmethodservice.InputMethodService; |
| import android.media.AudioManager; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.Vibrator; |
| import android.preference.PreferenceManager; |
| import android.text.InputType; |
| import android.text.SpannableStringBuilder; |
| import android.text.Spanned; |
| import android.text.style.BackgroundColorSpan; |
| import android.text.style.CharacterStyle; |
| import android.text.style.UnderlineSpan; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.inputmethod.CursorAnchorInfo; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputBinding; |
| import android.view.inputmethod.InputConnection; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.EnumMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.annotation.Nullable; |
| |
| |
| /** |
| * The input method service. |
| * |
| */ |
| public class MozcService extends InputMethodService { |
| |
| /** |
| * InputMethod implementation for MozcService. |
| * This injects the composing text tracking feature by wrapping InputConnection. |
| */ |
| public class MozcInputMethod extends InputMethodService.InputMethodImpl { |
| @Override |
| public void bindInput(InputBinding binding) { |
| binding = new InputBinding( |
| ComposingTextTrackingInputConnection.newInstance(binding.getConnection()), |
| binding.getConnectionToken(), |
| binding.getUid(), |
| binding.getPid()); |
| super.bindInput(binding); |
| } |
| |
| @Override |
| public void startInput(InputConnection inputConnection, EditorInfo attribute) { |
| super.startInput( |
| ComposingTextTrackingInputConnection.newInstance(inputConnection), attribute); |
| } |
| |
| @Override |
| public void restartInput(InputConnection inputConnection, EditorInfo attribute) { |
| super.restartInput( |
| ComposingTextTrackingInputConnection.newInstance(inputConnection), attribute); |
| } |
| } |
| |
| private static class RealFeedbackListener implements FeedbackListener { |
| private final Vibrator vibrator; |
| private final AudioManager audioManager; |
| |
| private RealFeedbackListener(Vibrator vibrator, AudioManager audioManager) { |
| if (vibrator == null) { |
| MozcLog.w("vibrator must be non-null. Vibration is disabled."); |
| } |
| this.vibrator = vibrator; |
| if (audioManager == null) { |
| MozcLog.w("audioManager must be non-null. Sound feedback is disabled."); |
| } |
| this.audioManager = audioManager; |
| } |
| |
| @Override |
| public void onVibrate(long duration) { |
| if (duration < 0) { |
| MozcLog.w("duration must be >= 0 but " + duration); |
| return; |
| } |
| if (vibrator != null) { |
| vibrator.vibrate(duration); |
| } |
| } |
| |
| @Override |
| public void onSound(int soundEffectType, float volume) { |
| if (audioManager != null && soundEffectType != FeedbackManager.FeedbackEvent.NO_SOUND) { |
| audioManager.playSoundEffect(soundEffectType, volume); |
| } |
| } |
| } |
| |
| /** Adapter implementation of the symbol history manipulation. */ |
| static class SymbolHistoryStorageImpl implements SymbolHistoryStorage { |
| static final Map<SymbolMajorCategory, StorageType> STORAGE_TYPE_MAP; |
| static { |
| Map<SymbolMajorCategory, StorageType> map = |
| new EnumMap<SymbolMajorCategory, StorageType>(SymbolMajorCategory.class); |
| map.put(SymbolMajorCategory.SYMBOL, StorageType.SYMBOL_HISTORY); |
| map.put(SymbolMajorCategory.EMOTICON, StorageType.EMOTICON_HISTORY); |
| map.put(SymbolMajorCategory.EMOJI, StorageType.EMOJI_HISTORY); |
| STORAGE_TYPE_MAP = Collections.unmodifiableMap(map); |
| } |
| |
| private final SessionExecutor sessionExecutor; |
| |
| SymbolHistoryStorageImpl(SessionExecutor sessionExecutor) { |
| this.sessionExecutor = sessionExecutor; |
| } |
| |
| @Override |
| public List<String> getAllHistory(SymbolMajorCategory majorCategory) { |
| List<ByteString> historyList = |
| sessionExecutor.readAllFromStorage(STORAGE_TYPE_MAP.get(majorCategory)); |
| List<String> result = new ArrayList<String>(historyList.size()); |
| for (ByteString value : historyList) { |
| result.add(MozcUtil.utf8CStyleByteStringToString(value)); |
| } |
| return result; |
| } |
| |
| @Override |
| public void addHistory(SymbolMajorCategory majorCategory, String value) { |
| Preconditions.checkNotNull(majorCategory); |
| Preconditions.checkNotNull(value); |
| sessionExecutor.insertToStorage( |
| STORAGE_TYPE_MAP.get(majorCategory), |
| value, |
| Collections.singletonList(ByteString.copyFromUtf8(value))); |
| } |
| } |
| |
| // Called back from ViewManager |
| @VisibleForTesting class MozcEventListener implements ViewEventListener { |
| @Override |
| public void onConversionCandidateSelected(int candidateId, Optional<Integer> rowIndex) { |
| sessionExecutor.submitCandidate(candidateId, rowIndex, renderResultCallback); |
| feedbackManager.fireFeedback(FeedbackEvent.CANDIDATE_SELECTED); |
| } |
| |
| @Override |
| public void onPageUp() { |
| sessionExecutor.pageUp(renderResultCallback); |
| feedbackManager.fireFeedback(FeedbackEvent.KEY_DOWN); |
| } |
| |
| @Override |
| public void onPageDown() { |
| sessionExecutor.pageDown(renderResultCallback); |
| feedbackManager.fireFeedback(FeedbackEvent.KEY_DOWN); |
| } |
| |
| @Override |
| public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String candidate, |
| boolean updateHistory) { |
| Preconditions.checkNotNull(majorCategory); |
| Preconditions.checkNotNull(candidate); |
| |
| // Directly commit the text. |
| commitText(candidate); |
| |
| if (updateHistory) { |
| symbolHistoryStorage.addHistory(majorCategory, candidate); |
| } |
| feedbackManager.fireFeedback(FeedbackEvent.CANDIDATE_SELECTED); |
| } |
| |
| private void commitText(String text) { |
| InputConnection inputConnection = getCurrentInputConnection(); |
| if (inputConnection == null) { |
| return; |
| } |
| inputConnection.beginBatchEdit(); |
| try { |
| inputConnection.commitText(text, MozcUtil.CURSOR_POSITION_TAIL); |
| } finally { |
| inputConnection.endBatchEdit(); |
| } |
| } |
| |
| @Override |
| public void onKeyEvent( |
| @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface keyEvent, |
| @Nullable KeyboardSpecification keyboardSpecification, List<TouchEvent> touchEventList) { |
| if (mozcKeyEvent == null && keyboardSpecification == null) { |
| // We don't send a key event to Mozc native layer since {@code mozcKeyEvent} is null, and we |
| // don't need to update the keyboard specification since {@code keyboardSpecification} is |
| // also null. |
| if (keyEvent == null) { |
| // Send a usage information to Mozc native layer. |
| sessionExecutor.touchEventUsageStatsEvent(touchEventList); |
| } else { |
| // Send a key event (which is generated by Mozc in the usual case) to application. |
| Preconditions.checkArgument(touchEventList.isEmpty()); |
| sessionExecutor.sendKeyEvent(keyEvent, sendKeyToApplicationCallback); |
| } |
| return; |
| } |
| |
| sendKeyWithKeyboardSpecification(mozcKeyEvent, keyEvent, |
| keyboardSpecification, getConfiguration(), |
| touchEventList); |
| } |
| |
| @Override |
| public void onUndo(List<TouchEvent> touchEventList) { |
| sessionExecutor.undoOrRewind(touchEventList, renderResultCallback); |
| } |
| |
| @Override |
| public void onFireFeedbackEvent(FeedbackEvent event) { |
| feedbackManager.fireFeedback(event); |
| if (event.equals(FeedbackEvent.INPUTVIEW_EXPAND)) { |
| sessionExecutor.sendUsageStatsEvent(UsageStatsEvent.KEYBOARD_EXPAND_EVENT); |
| } else if (event.equals(FeedbackEvent.INPUTVIEW_FOLD)) { |
| sessionExecutor.sendUsageStatsEvent(UsageStatsEvent.KEYBOARD_FOLD_EVENT); |
| } |
| } |
| |
| @Override |
| public void onSubmitPreedit() { |
| sessionExecutor.submit(renderResultCallback); |
| } |
| |
| @Override |
| public void onExpandSuggestion() { |
| sessionExecutor.expandSuggestion(renderResultCallback); |
| } |
| |
| @Override |
| public void onShowMenuDialog(List<TouchEvent> touchEventList) { |
| sessionExecutor.touchEventUsageStatsEvent(touchEventList); |
| } |
| |
| @Override |
| public void onShowSymbolInputView(List<TouchEvent> touchEventList) { |
| changeKeyboardSpecificationAndSendKey( |
| null, null, KeyboardSpecification.SYMBOL_NUMBER, getConfiguration(), |
| Collections.<TouchEvent>emptyList()); |
| viewManager.onShowSymbolInputView(); |
| } |
| |
| @Override |
| public void onCloseSymbolInputView() { |
| viewManager.onCloseSymbolInputView(); |
| // This callback is called in two ways: one is from touch event on symbol input view. |
| // The other is from onKeyDown event by hardware keyboard. ViewManager.isNarrowMode() |
| // is abused to distinguish these two triggers where its true value indicates that |
| // onCloseSymbolInputView() is called on hardware keyboard event. In the case of hardware |
| // keyboard event, keyboard specification has been already updated so we shouldn't update it. |
| if (!viewManager.isNarrowMode()) { |
| changeKeyboardSpecificationAndSendKey( |
| null, null, viewManager.getKeyboardSpecification(), getConfiguration(), |
| Collections.<TouchEvent>emptyList()); |
| } |
| } |
| |
| @Override |
| public void onHardwareKeyboardCompositionModeChange(CompositionSwitchMode mode) { |
| viewManager.switchHardwareKeyboardCompositionMode(mode); |
| } |
| |
| @Override |
| public void onActionKey() { |
| // false means that the key is for Action and not ENTER. |
| sendEditorAction(false); |
| } |
| |
| @Override |
| public void onNarrowModeChanged(boolean newNarrowMode) { |
| if (!newNarrowMode) { |
| // Hardware keyboard to software keyboard transition: Submit composition. |
| sessionExecutor.submit(renderResultCallback); |
| } |
| updateImposedConfig(); |
| } |
| |
| @Override |
| public void onUpdateKeyboardLayoutAdjustment( |
| ViewManagerInterface.LayoutAdjustment layoutAdjustment) { |
| Preconditions.checkNotNull(layoutAdjustment); |
| Configuration configuration = getConfiguration(); |
| if (sharedPreferences == null || configuration == null) { |
| return; |
| } |
| boolean isLandscapeKeyboardSettingActive = |
| PreferenceUtil.isLandscapeKeyboardSettingActive( |
| sharedPreferences, configuration.orientation); |
| String key; |
| if (isLandscapeKeyboardSettingActive) { |
| key = PreferenceUtil.PREF_LANDSCAPE_LAYOUT_ADJUSTMENT_KEY; |
| } else { |
| key = PreferenceUtil.PREF_PORTRAIT_LAYOUT_ADJUSTMENT_KEY; |
| } |
| sharedPreferences.edit() |
| .putString(key, layoutAdjustment.toString()) |
| .apply(); |
| } |
| } |
| |
| /** |
| * Callback to render the result received from Mozc server. |
| */ |
| private class RenderResultCallback implements SessionExecutor.EvaluationCallback { |
| |
| @Override |
| public void onCompleted( |
| Optional<Command> command, Optional<KeyEventInterface> triggeringKeyEvent) { |
| Preconditions.checkArgument(Preconditions.checkNotNull(command).isPresent()); |
| Preconditions.checkNotNull(triggeringKeyEvent); |
| if (command.get().getInput().getCommand().getType() |
| != SessionCommand.CommandType.EXPAND_SUGGESTION) { |
| // For expanding suggestions, we don't need to update our rendering result. |
| renderInputConnection(command.get(), triggeringKeyEvent.orNull()); |
| } |
| // Transit to narrow mode if required (e.g., Typed 'a' key from h/w keyboard). |
| viewManager.maybeTransitToNarrowMode(command.get(), triggeringKeyEvent.orNull()); |
| viewManager.render(command.get()); |
| } |
| } |
| |
| /** |
| * Callback to send key event to a application. |
| */ |
| @VisibleForTesting |
| class SendKeyToApplicationCallback implements SessionExecutor.EvaluationCallback { |
| |
| @Override |
| public void onCompleted(Optional<Command> command, |
| Optional<KeyEventInterface> triggeringKeyEvent) { |
| Preconditions.checkArgument(!Preconditions.checkNotNull(command).isPresent()); |
| sendKeyEvent(triggeringKeyEvent.orNull()); |
| } |
| } |
| |
| /** |
| * Callback to send key event to view layer. |
| */ |
| private class SendKeyToViewCallback implements SessionExecutor.EvaluationCallback { |
| |
| @Override |
| public void onCompleted( |
| Optional<Command> command, Optional<KeyEventInterface> triggeringKeyEvent) { |
| Preconditions.checkArgument(!Preconditions.checkNotNull(command).isPresent()); |
| Preconditions.checkArgument(Preconditions.checkNotNull(triggeringKeyEvent).isPresent()); |
| viewManager.consumeKeyOnViewSynchronously(triggeringKeyEvent.get().getNativeEvent().orNull()); |
| } |
| } |
| |
| /** |
| * Callback to invoke onUpdateSelectionInternal with delay for onConfigurationChanged. |
| * See onConfigurationChanged for the details. |
| */ |
| private class ConfigurationChangeCallback implements Handler.Callback { |
| @Override |
| public boolean handleMessage(Message msg) { |
| int selectionStart = msg.arg1; |
| int selectionEnd = msg.arg2; |
| onUpdateSelectionInternal(selectionStart, selectionEnd, selectionStart, selectionEnd, -1, -1); |
| return true; |
| } |
| } |
| |
| /** |
| * We need to send SYNC_DATA command periodically. This class handles it. |
| */ |
| @SuppressLint("HandlerLeak") |
| private class SendSyncDataCommandHandler extends Handler { |
| /** |
| * The current period of sending SYNC_DATA is 15 mins (as same as desktop version). |
| */ |
| static final int SYNC_DATA_COMMAND_PERIOD = 15 * 60 * 1000; |
| |
| @Override |
| public void handleMessage(Message msg) { |
| if (sessionExecutor != null) { |
| sessionExecutor.syncData(); |
| } |
| sendEmptyMessageDelayed(0, SYNC_DATA_COMMAND_PERIOD); |
| } |
| } |
| |
| /** |
| * To trim memory, a message is handled to invoke trimMemory method |
| * 10 seconds after hiding window. |
| * |
| * This class handles callback operation. |
| * Posting and removing messages should be done in appropriate point. |
| */ |
| @SuppressLint("HandlerLeak") |
| private class MemoryTrimmingHandler extends Handler { |
| |
| /** |
| * "what" value of message. Always use this. |
| */ |
| static final int WHAT = 0; |
| |
| /** |
| * Duration after hiding window in milliseconds. |
| */ |
| static final int DURATION_MS = 10 * 1000; |
| |
| @Override |
| public void handleMessage(Message msg) { |
| trimMemory(); |
| // Other messages in the queue are removed as they will do the same thing |
| // and will affect nothing. |
| removeMessages(WHAT); |
| } |
| } |
| |
| |
| // Keys for tweak preferences. |
| private static final String PREF_TWEAK_PREFIX = "pref_tweak_"; |
| private static final String PREF_TWEAK_LOGGING_PROTOCOL_BUFFERS = |
| "pref_tweak_logging_protocol_buffers"; |
| |
| // Focused segment's attribute. |
| @VisibleForTesting static final CharacterStyle SPAN_CONVERT_HIGHLIGHT = |
| new BackgroundColorSpan(0x66EF3566); |
| |
| // Background color span for non-focused conversion segment. |
| // We don't create a static CharacterStyle instance since there are multiple segments at the same |
| // time. Otherwise, segments except for the last one cannot have style. |
| @VisibleForTesting static final int CONVERT_NORMAL_COLOR = 0x19EF3566; |
| |
| // Cursor position. |
| // Note that InputConnection seems not to be able to show cursor. This is a workaround. |
| @VisibleForTesting static final CharacterStyle SPAN_BEFORE_CURSOR = |
| new BackgroundColorSpan(0x664DB6AC); |
| |
| // Background color span for partial conversion. |
| @VisibleForTesting static final CharacterStyle SPAN_PARTIAL_SUGGESTION_COLOR = |
| new BackgroundColorSpan(0x194DB6AC); |
| |
| // Underline. |
| @VisibleForTesting static final CharacterStyle SPAN_UNDERLINE = new UnderlineSpan(); |
| |
| // Mozc's session. All session related task should be done via this instance. |
| @VisibleForTesting SessionExecutor sessionExecutor; |
| @VisibleForTesting final RenderResultCallback renderResultCallback = new RenderResultCallback(); |
| @VisibleForTesting final SendKeyToApplicationCallback sendKeyToApplicationCallback = |
| new SendKeyToApplicationCallback(); |
| private final SendKeyToViewCallback sendKeyToViewCallback = new SendKeyToViewCallback(); |
| |
| // A manager for all views and feedbacks. |
| @VisibleForTesting |
| public ViewManagerInterface viewManager; |
| @VisibleForTesting FeedbackManager feedbackManager; |
| @VisibleForTesting SymbolHistoryStorage symbolHistoryStorage; |
| |
| @VisibleForTesting SharedPreferences sharedPreferences; |
| |
| // A handler for onSharedPreferenceChanged(). |
| // Note: the handler is needed to be held by the service not to be GC'ed. |
| @VisibleForTesting final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = |
| new SharedPreferenceChangeAdapter(); |
| |
| // Preference information which are propagated. Null if not propagated yet. |
| @VisibleForTesting ClientSidePreference propagatedClientSidePreference = null; |
| |
| // Track the selection. |
| @VisibleForTesting SelectionTracker selectionTracker = new SelectionTracker(); |
| |
| // A receiver to accept a notification via intents. |
| @VisibleForTesting Handler configurationChangedHandler = |
| new Handler(new ConfigurationChangeCallback()); |
| |
| // Handler to process SYNC_DATA command for storing history data. |
| @VisibleForTesting Handler sendSyncDataCommandHandler = new SendSyncDataCommandHandler(); |
| |
| // Handler to process SYNC_DATA command for storing history data. |
| private final Handler memoryTrimmingHandler = new MemoryTrimmingHandler(); |
| |
| // This is a cache of MozcUtil.isDebug. It will be set in onCreateInternal and |
| // will never be changed. |
| private boolean isDebugBuild; |
| |
| // Current KeyboardSpecification, which is determined by the last key event. |
| // Note that this might be different from what a user sees. |
| // For example when a user is in narrow mode (this field is for H/W keyboard) |
| // and (s)he taps widen button to see S/W keyboard, |
| // (s)he will see S/W keyboard but this field keep to point H/W keyboard because |
| // widen button is not a key event. |
| // This behavior is error-prone and might be a kind of bug. At least the name doesn't represent |
| // the behavior. |
| // TODO(matsuzakit): Clarify the usage of this field (change the behavior to keep the latest |
| // state or change the name to represent current behavior). |
| @VisibleForTesting KeyboardSpecification currentKeyboardSpecification = |
| KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA; |
| |
| // Current HardKeyboardHidden configuration value. |
| // This is updated only when onConfigurationChanged is called and |
| // Configuration.HARDKEYBOARDHIDDEN_* differs to this. |
| private int currentHardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_UNDEFINED; |
| |
| @VisibleForTesting boolean inputBound = false; |
| |
| private ApplicationCompatibility applicationCompatibility = |
| ApplicationCompatibility.getDefaultInstance(); |
| |
| // Listener called by views. |
| // Held for testing. |
| private ViewEventListener eventListener; |
| |
| @SuppressWarnings("deprecation") |
| @SuppressLint("NewApi") |
| public MozcService() { |
| super(); |
| if (Build.VERSION.SDK_INT >= 17) { |
| enableHardwareAcceleration(); |
| } |
| } |
| |
| @Override |
| public void onBindInput() { |
| super.onBindInput(); |
| inputBound = true; |
| } |
| |
| @Override |
| public void onUnbindInput() { |
| inputBound = false; |
| super.onUnbindInput(); |
| } |
| |
| @Override |
| public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { |
| if (viewManager != null) { |
| viewManager.setCursorAnchorInfo(cursorAnchorInfo); |
| } |
| } |
| |
| @Override |
| public MozcInputMethod onCreateInputMethodInterface() { |
| return new MozcInputMethod(); |
| } |
| |
| @Override |
| public void onCreate() { |
| // Note: super.onCreate() is invoked in onCreateInternal. So, do not call it, here. |
| MozcLog.d("start MozcService#onCreate " + System.nanoTime()); |
| |
| // TODO(hidehiko): Restructure around initialization code in order to make tests stable. |
| // Callback object mainly used by views. |
| MozcEventListener eventListener = new MozcEventListener(); |
| SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); |
| Preconditions.checkNotNull(sharedPreferences); |
| SessionExecutor sessionExecutor = |
| SessionExecutor.getInstanceInitializedIfNecessary( |
| new SessionHandlerFactory(Optional.of(sharedPreferences)), this); |
| onCreateInternal(eventListener, null, sharedPreferences, getConfiguration(), |
| sessionExecutor); |
| |
| MozcLog.d("end MozcService#onCreate " + System.nanoTime()); |
| } |
| |
| @Override |
| public void onDestroy() { |
| feedbackManager.release(); |
| if (sessionExecutor != null) { |
| sessionExecutor.syncData(); |
| } |
| super.onDestroy(); |
| } |
| |
| @VisibleForTesting |
| void onCreateInternal(ViewEventListener eventListener, @Nullable ViewManagerInterface viewManager, |
| @Nullable SharedPreferences sharedPreferences, |
| Configuration deviceConfiguration, SessionExecutor sessionExecutor) { |
| super.onCreate(); |
| |
| Context context = getApplicationContext(); |
| isDebugBuild = MozcUtil.isDebug(context); |
| |
| // TODO(hidehiko): Split following methods by functionalities to improve test coverage and |
| // test stableness. |
| this.sessionExecutor = sessionExecutor; |
| this.symbolHistoryStorage = new SymbolHistoryStorageImpl(sessionExecutor); |
| this.eventListener = eventListener; |
| prepareOnce(eventListener, symbolHistoryStorage, viewManager, sharedPreferences); |
| prepareEveryTime(sharedPreferences, deviceConfiguration); |
| |
| if (propagatedClientSidePreference == null |
| || propagatedClientSidePreference.getHardwareKeyMap() == null) { |
| HardwareKeyboardSpecification.maybeSetDetectedHardwareKeyMap( |
| sharedPreferences, deviceConfiguration, false); |
| } |
| |
| // Start sending SYNC_DATA message to mozc server periodically. |
| sendSyncDataCommandHandler.sendEmptyMessageDelayed( |
| 0, SendSyncDataCommandHandler.SYNC_DATA_COMMAND_PERIOD); |
| this.sharedPreferences = sharedPreferences; |
| } |
| |
| /** |
| * Prepares something which should be done every time when the session is newly created. |
| */ |
| private void prepareEveryTime( |
| @Nullable SharedPreferences sharedPreferences, Configuration deviceConfiguration) { |
| boolean isLogging = sharedPreferences != null |
| && sharedPreferences.getBoolean(PREF_TWEAK_LOGGING_PROTOCOL_BUFFERS, false); |
| // Force to initialize here. |
| sessionExecutor.reset( |
| new SessionHandlerFactory(Optional.fromNullable(sharedPreferences)), this); |
| sessionExecutor.setLogging(isLogging); |
| |
| updateImposedConfig(); |
| viewManager.onConfigurationChanged(getConfiguration()); |
| // Make sure that the server and the client have the same keyboard specification. |
| // User preference's keyboard will be set after this step. |
| changeKeyboardSpecificationAndSendKey( |
| null, null, currentKeyboardSpecification, deviceConfiguration, |
| Collections.<TouchEvent>emptyList()); |
| if (sharedPreferences != null) { |
| propagateClientSidePreference( |
| new ClientSidePreference( |
| sharedPreferences, getResources(), deviceConfiguration.orientation)); |
| // TODO(hidehiko): here we just set the config based on preferences. When we start |
| // to support sync on Android, we need to revisit the config related design. |
| sessionExecutor.setConfig(ConfigUtil.toConfig(sharedPreferences)); |
| sessionExecutor.preferenceUsageStatsEvent(sharedPreferences, getResources()); |
| } |
| |
| maybeSetNarrowMode(deviceConfiguration); |
| } |
| |
| /** |
| * Prepares something which should be done only once. |
| */ |
| private void prepareOnce(ViewEventListener eventListener, |
| SymbolHistoryStorage symbolHistoryStorage, |
| @Nullable ViewManagerInterface viewManager, |
| @Nullable SharedPreferences sharedPreferences) { |
| Context context = getApplicationContext(); |
| Optional<Intent> forwardIntent = |
| ApplicationInitializerFactory.createInstance(this).initialize( |
| MozcUtil.isSystemApplication(context), |
| MozcUtil.isDevChannel(context), |
| DependencyFactory.getDependency(getApplicationContext()).isWelcomeActivityPreferrable(), |
| MozcUtil.getAbiIndependentVersionCode(context), |
| LauncherIconManagerFactory.getDefaultInstance(), |
| PreferenceUtil.getDefaultPreferenceManagerStatic()); |
| if (forwardIntent.isPresent()) { |
| startActivity(forwardIntent.get()); |
| } |
| |
| // Create a ViewManager. |
| if (viewManager == null) { |
| ImeSwitcher imeSwitcher = ImeSwitcherFactory.getImeSwitcher(this); |
| viewManager = DependencyFactory.getDependency( |
| getApplicationContext()).createViewManager( |
| getApplicationContext(), |
| eventListener, |
| symbolHistoryStorage, |
| imeSwitcher, |
| new MozcMenuDialogListenerImpl(this)); |
| } |
| |
| // Setup FeedbackManager. |
| feedbackManager = new FeedbackManager(new RealFeedbackListener( |
| Vibrator.class.cast(getSystemService(Context.VIBRATOR_SERVICE)), |
| AudioManager.class.cast(getSystemService(Context.AUDIO_SERVICE)))); |
| |
| this.viewManager = viewManager; |
| |
| // Set a callback for preference changing. |
| if (sharedPreferences != null) { |
| sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); |
| } |
| } |
| |
| @Override |
| public boolean onEvaluateInputViewShown() { |
| // TODO(matsuzakit): Implement me |
| return true; |
| } |
| |
| @Override |
| public View onCreateInputView() { |
| MozcLog.d("start MozcService#onCreateInputView " + System.nanoTime()); |
| View inputView = viewManager.createMozcView(this); |
| MozcLog.d("end MozcService#onCreateInputView " + System.nanoTime()); |
| return inputView; |
| } |
| |
| private void resetContext() { |
| if (sessionExecutor != null) { |
| sessionExecutor.resetContext(); |
| } |
| if (viewManager != null) { |
| viewManager.reset(); |
| } |
| } |
| |
| @Override |
| public void onFinishInput() { |
| // Omit rendering because the input view will soon disappear. |
| resetContext(); |
| selectionTracker.onFinishInput(); |
| applicationCompatibility = ApplicationCompatibility.getDefaultInstance(); |
| super.onFinishInput(); |
| } |
| |
| @Override |
| public void onStartInput(EditorInfo attribute, boolean restarting) { |
| super.onStartInput(attribute, restarting); |
| |
| applicationCompatibility = ApplicationCompatibility.getInstance(attribute); |
| |
| // Update full screen mode, because the application may be changed. |
| viewManager.setFullscreenMode( |
| applicationCompatibility.isFullScreenModeSupported() |
| && propagatedClientSidePreference != null |
| && propagatedClientSidePreference.isFullscreenMode()); |
| |
| // Some applications, e.g. gmail or maps, send onStartInput with restarting = true, when a user |
| // rotates a device. In such cases, we don't want to update caret positions, nor reset |
| // the context basically. However, some other applications, such as one with a webview widget |
| // like a browser, send onStartInput with restarting = true, too. Unfortunately, |
| // there seems no way to figure out which one causes this invocation. |
| // So, as a point of compromise, we reset the context every time here. Also, we'll send |
| // finishComposingText as well, in case the new attached field has already had composing text |
| // (we hit such a situation on webview, too). |
| // See also onConfigurationChanged for caret position handling on gmail-like applications' |
| // device rotation events. |
| resetContext(); |
| InputConnection connection = getCurrentInputConnection(); |
| if (connection != null) { |
| connection.finishComposingText(); |
| maybeCommitMushroomResult(attribute, connection); |
| } |
| |
| // Send the connected field's attributes to the mozc server. |
| sessionExecutor.switchInputFieldType(getInputFieldType(attribute)); |
| sessionExecutor.updateRequest( |
| EmojiUtil.createEmojiRequest( |
| Build.VERSION.SDK_INT, |
| (propagatedClientSidePreference != null && EmojiUtil.isCarrierEmojiAllowed(attribute)) |
| ? propagatedClientSidePreference.getEmojiProviderType() : EmojiProviderType.NONE), |
| Collections.<TouchEvent>emptyList()); |
| selectionTracker.onStartInput( |
| attribute.initialSelStart, attribute.initialSelEnd, isWebEditText(attribute)); |
| } |
| |
| /** |
| * Hook to support mushroom protocol. If there is pending Mushroom result for the connecting |
| * field, commit it. Then, (regardless of whether there exists pending result,) clears |
| * all remaining pending result. |
| */ |
| private static void maybeCommitMushroomResult(EditorInfo attribute, InputConnection connection) { |
| if (connection == null) { |
| return; |
| } |
| |
| MushroomResultProxy resultProxy = MushroomResultProxy.getInstance(); |
| String result; |
| synchronized (resultProxy) { |
| // We need to obtain the result and then clear the all remaining result atomically. |
| result = resultProxy.getReplaceKey(attribute.fieldId); |
| resultProxy.clear(); |
| } |
| if (result != null) { |
| // Found the pending mushroom application result to the connecting field. Commit it. |
| connection.commitText(result, MozcUtil.CURSOR_POSITION_TAIL); |
| } |
| } |
| |
| @SuppressLint("NewApi") |
| private static boolean enableCursorAnchorInfo(InputConnection connection) { |
| Preconditions.checkNotNull(connection); |
| if (Build.VERSION.SDK_INT < 21) { |
| return false; |
| } |
| return connection.requestCursorUpdates( |
| InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR); |
| } |
| |
| /** |
| * @return true if connected view is WebEditText (or the application pretends it) |
| */ |
| private boolean isWebEditText(EditorInfo editorInfo) { |
| if (editorInfo == null) { |
| return false; |
| } |
| |
| if (applicationCompatibility.isPretendingWebEditText()) { |
| return true; |
| } |
| |
| // TODO(hidehiko): Refine the heuristic to check isWebEditText related stuff. |
| MozcLog.d("inputType: " + editorInfo.inputType); |
| int variation = editorInfo.inputType & InputType.TYPE_MASK_VARIATION; |
| return variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; |
| } |
| |
| @Override |
| public void onStartInputView(EditorInfo attribute, boolean restarting) { |
| InputConnection inputConnection = getCurrentInputConnection(); |
| if (inputConnection != null && Build.VERSION.SDK_INT >= 21) { |
| viewManager.setCursorAnchorInfoEnabled(enableCursorAnchorInfo(inputConnection)); |
| updateImposedConfig(); |
| } |
| |
| viewManager.setTextForActionButton(getTextForImeAction(attribute.imeOptions)); |
| viewManager.setEditorInfo(attribute); |
| // updateXxxxxButtonEnabled cannot be placed in onStartInput because |
| // the view might be created after onStartInput with *reset* status. |
| viewManager.updateGlobeButtonEnabled(); |
| viewManager.updateMicrophoneButtonEnabled(); |
| } |
| |
| static InputFieldType getInputFieldType(EditorInfo attribute) { |
| int inputType = attribute.inputType; |
| if (MozcUtil.isPasswordField(inputType)) { |
| return InputFieldType.PASSWORD; |
| } |
| int inputClass = inputType & InputType.TYPE_MASK_CLASS; |
| if (inputClass == InputType.TYPE_CLASS_PHONE) { |
| return InputFieldType.TEL; |
| } |
| if (inputClass == InputType.TYPE_CLASS_NUMBER) { |
| return InputFieldType.NUMBER; |
| } |
| return InputFieldType.NORMAL; |
| } |
| |
| @Override |
| public void onComputeInsets(InputMethodService.Insets outInsets) { |
| viewManager.computeInsets(getApplicationContext(), outInsets, getWindow().getWindow()); |
| } |
| |
| /** |
| * KeyDown event handler. |
| * |
| * This method is called only by the android framework e.g HOME,BACK or H/W keyboard input. |
| */ |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| return onKeyDownInternal(keyCode, event, getConfiguration()); |
| } |
| |
| /** |
| * GenericMotionEvent handler. |
| * |
| * This method is called only by the android framework e.g H/W mouse, touch pad input, etc. |
| */ |
| @TargetApi(17) |
| @Override |
| public boolean onGenericMotionEvent(MotionEvent event) { |
| if (!isInputViewShown()) { |
| return super.onGenericMotionEvent(event); |
| } |
| if (viewManager.isGenericMotionToConsume(event)) { |
| return viewManager.consumeGenericMotion(event); |
| } |
| return super.onGenericMotionEvent(event); |
| } |
| |
| @SuppressLint("DefaultLocale") |
| @VisibleForTesting |
| boolean onKeyDownInternal(int keyCode, KeyEvent event, Configuration configuration) { |
| if (MozcLog.isLoggable(Log.DEBUG)) { |
| MozcLog.d( |
| String.format( |
| "onKeyDown keyCode:0x%x, metaState:0x%x, scanCode:0x%x, uniCode:0x%x, deviceId:%d", |
| event.getKeyCode(), event.getMetaState(), event.getScanCode(), |
| event.getUnicodeChar(), event.getDeviceId())); |
| } |
| |
| // Send back the event if the input view is not shown. |
| if (!isInputViewShown()) { |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| // Send back the event if the event is one of the system keys, which invoke |
| // system's action. (e.g. back, home, power, volume and so on). |
| if (event.isSystem()) { |
| // Special handle for back key. We need to post it to the server and maybeProcessBackKey |
| // should handle it later. The posting the event is done in onKeyUp, so we just consume the |
| // down key event here. |
| if (keyCode == KeyEvent.KEYCODE_BACK) { |
| return true; |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| // Push the event to the asynchronous execution queue if it should be processed |
| // directly in the view. |
| if (viewManager.isKeyConsumedOnViewAsynchronously(event)) { |
| sessionExecutor.sendKeyEvent(KeycodeConverter.getKeyEventInterface(event), |
| sendKeyToViewCallback); |
| return true; |
| } |
| |
| // Lazy evaluation. |
| // If hardware keyboard is not set in the preference screen, |
| // set it based on the configuration. |
| if (propagatedClientSidePreference == null |
| || propagatedClientSidePreference.getHardwareKeyMap() == null) { |
| HardwareKeyboardSpecification.maybeSetDetectedHardwareKeyMap( |
| sharedPreferences, configuration, true); |
| } |
| |
| // Here we decided to send the event to the server. |
| |
| viewManager.onHardwareKeyEvent(event); |
| return true; |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (isInputViewShown()) { |
| if (event.isSystem()) { |
| // The back key should be processed as same as the meta keys. |
| // See also comments described below. |
| if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { |
| sessionExecutor.sendKeyEvent( |
| KeycodeConverter.getKeyEventInterface(event), sendKeyToApplicationCallback); |
| return true; |
| } |
| } else { |
| if (viewManager.isKeyConsumedOnViewAsynchronously(event)) { |
| sessionExecutor.sendKeyEvent(KeycodeConverter.getKeyEventInterface(event), |
| sendKeyToViewCallback); |
| return true; |
| } |
| |
| // The IME is active and the server can handle the event so consume the event. |
| // Currently the server does not consume UP event for not meta keys. |
| // For meta keys, to guarantee that this up event is sent to InputConnection after down key, |
| // this is sent to evaluation handler. |
| if (KeycodeConverter.isMetaKey(event)) { |
| sessionExecutor.sendKeyEvent(KeycodeConverter.getKeyEventInterface(event), |
| sendKeyToApplicationCallback); |
| } |
| return true; |
| } |
| } |
| |
| // If the IME is turned off or the event should not be sent to the server, |
| // delegate to the super class. |
| // Note that delegation should be done only when needed. |
| // For example hardware keyboard's enter key should not be delegated |
| // because its DOWN event is sent to the sever. |
| // If delegated, enter key event is sent to the application twice (DOWN and UP). |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| /** |
| * Sends mozcKeyEvent and/or Request to mozc server. |
| * |
| * This skips to send request if the given keyboard specification is same as before. |
| */ |
| @VisibleForTesting |
| void sendKeyWithKeyboardSpecification( |
| @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface event, |
| @Nullable KeyboardSpecification keyboardSpecification, Configuration configuration, |
| List<TouchEvent> touchEventList) { |
| if (keyboardSpecification != null && currentKeyboardSpecification != keyboardSpecification) { |
| // Submit composition on the transition from software KB to hardware KB by key event. |
| if (!currentKeyboardSpecification.isHardwareKeyboard() |
| && keyboardSpecification.isHardwareKeyboard()) { |
| sessionExecutor.submit(renderResultCallback); |
| } |
| changeKeyboardSpecificationAndSendKey( |
| mozcKeyEvent, event, keyboardSpecification, configuration, touchEventList); |
| updateStatusIcon(); |
| } else if (mozcKeyEvent != null) { |
| // Send mozcKeyEvent as usual. |
| sessionExecutor.sendKey(mozcKeyEvent, event, touchEventList, renderResultCallback); |
| } else if (event != null) { |
| // Send event back to the application to handle key events which cannot be converted into Mozc |
| // key event (e.g. Shift) correctly. |
| sessionExecutor.sendKeyEvent(event, sendKeyToApplicationCallback); |
| } |
| } |
| |
| /** |
| * Sends Request for changing keyboard setting to mozc server and sends key. |
| */ |
| private void changeKeyboardSpecificationAndSendKey( |
| @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface event, |
| KeyboardSpecification keyboardSpecification, Configuration configuration, |
| List<TouchEvent> touchEventList) { |
| // Send Request to change composition table. |
| sessionExecutor.updateRequest( |
| MozcUtil.getRequestBuilder(getResources(), keyboardSpecification, configuration).build(), |
| touchEventList); |
| if (mozcKeyEvent == null) { |
| // Change composition mode. |
| sessionExecutor.switchInputMode( |
| Optional.fromNullable(event), keyboardSpecification.getCompositionMode(), |
| renderResultCallback); |
| } else { |
| // Send key with composition mode change. |
| sessionExecutor.sendKey( |
| ProtoCommands.KeyEvent.newBuilder(mozcKeyEvent) |
| .setMode(keyboardSpecification.getCompositionMode()).build(), |
| event, touchEventList, renderResultCallback); |
| } |
| currentKeyboardSpecification = keyboardSpecification; |
| } |
| |
| /** |
| * Shows/Hides status icon according to the input view status. |
| */ |
| private void updateStatusIcon() { |
| if (isInputViewShown()) { |
| showStatusIcon(); |
| } else { |
| hideStatusIcon(); |
| } |
| } |
| |
| /** |
| * Shows the status icon basing on the current keyboard spec. |
| */ |
| private void showStatusIcon() { |
| switch (currentKeyboardSpecification.getCompositionMode()) { |
| case HIRAGANA: |
| showStatusIcon(R.drawable.status_icon_hiragana); |
| break; |
| default: |
| showStatusIcon(R.drawable.status_icon_alphabet); |
| break; |
| } |
| } |
| |
| @Override |
| public boolean onEvaluateFullscreenMode() { |
| return viewManager.isFullscreenMode(); |
| } |
| |
| @Override |
| public boolean onShowInputRequested(int flags, boolean configChange) { |
| boolean result = super.onShowInputRequested(flags, configChange); |
| boolean isHardwareKeyboardConnected = |
| getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS; |
| // Original result becomes false when a hardware keyboard is connected. |
| // This means that the window won't be shown in such situation. |
| // We want to show it even with a hardware keyboard so override the result here. |
| return result || isHardwareKeyboardConnected; |
| } |
| |
| @Override |
| public void onWindowShown() { |
| showStatusIcon(); |
| // Remove memory trimming message. |
| memoryTrimmingHandler.removeMessages(MemoryTrimmingHandler.WHAT); |
| // Ensure keyboard's request. |
| // The session might be deleted by trimMemory caused by onWindowHidden. |
| // Note that this logic must be placed *after* removing the messages in memoryTrimmingHandler. |
| // Otherwise the session might be unexpectedly deleted and newly re-created one will be used |
| // without appropriate request which is sent below. |
| changeKeyboardSpecificationAndSendKey( |
| null, null, currentKeyboardSpecification, getConfiguration(), |
| Collections.<TouchEvent>emptyList()); |
| } |
| |
| @Override |
| public void onWindowHidden() { |
| // "Hiding IME's window" is very similar to "Turning off IME" for PC. |
| // Thus |
| // - Committing composing text. |
| // - Removing all pending messages. |
| // - Resetting Mozc server |
| // are needed. |
| sessionExecutor.removePendingEvaluations(); |
| |
| resetContext(); |
| selectionTracker.onWindowHidden(); |
| viewManager.reset(); |
| hideStatusIcon(); |
| // MemoryTrimmingHandler.DURATION_MS from now, memory trimming will be done. |
| // If the window is shown before MemoryTrimmingHandler.DURATION_MS, |
| // the message posted here will be removed. |
| memoryTrimmingHandler.removeMessages(MemoryTrimmingHandler.WHAT); |
| memoryTrimmingHandler.sendEmptyMessageDelayed(MemoryTrimmingHandler.WHAT, |
| MemoryTrimmingHandler.DURATION_MS); |
| super.onWindowHidden(); |
| } |
| |
| /** |
| * Updates InputConnection. |
| * |
| * @param command Output message. Rendering is based on this parameter. |
| * @param keyEvent Trigger event for this calling. When direct input is |
| * needed, this event is sent to InputConnection. |
| */ |
| @VisibleForTesting |
| void renderInputConnection(Command command, @Nullable KeyEventInterface keyEvent) { |
| Preconditions.checkNotNull(command); |
| |
| InputConnection inputConnection = getCurrentInputConnection(); |
| if (inputConnection == null) { |
| return; |
| } |
| |
| Output output = command.getOutput(); |
| if (!output.hasConsumed() || !output.getConsumed()) { |
| maybeCommitText(output, inputConnection); |
| sendKeyEvent(keyEvent); |
| return; |
| } |
| |
| // Meta key may invoke a command for Mozc server like SWITCH_INPUT_MODE session command. In this |
| // case, the command is consumed by Mozc server and the application cannot get the key event. |
| // To avoid such situation, we should send the key event back to application. b/13238551 |
| // The command itself is consumed by Mozc server, so we should NOT put a return statement here. |
| if (keyEvent != null && keyEvent.getNativeEvent().isPresent() |
| && KeycodeConverter.isMetaKey(keyEvent.getNativeEvent().get())) { |
| sendKeyEvent(keyEvent); |
| } |
| |
| // Here the key is consumed by the Mozc server. |
| inputConnection.beginBatchEdit(); |
| try { |
| maybeDeleteSurroundingText(output, inputConnection); |
| maybeCommitText(output, inputConnection); |
| setComposingText(command, inputConnection); |
| maybeSetSelection(output, inputConnection); |
| selectionTracker.onRender( |
| output.hasDeletionRange() ? output.getDeletionRange() : null, |
| output.hasResult() ? output.getResult().getValue() : null, |
| output.hasPreedit() ? output.getPreedit() : null); |
| } finally { |
| inputConnection.endBatchEdit(); |
| } |
| } |
| |
| private static KeyEvent createKeyEvent( |
| KeyEvent original, long eventTime, int action, int repeatCount) { |
| return new KeyEvent( |
| original.getDownTime(), eventTime, action, original.getKeyCode(), |
| repeatCount, original.getMetaState(), original.getDeviceId(), original.getScanCode(), |
| original.getFlags()); |
| } |
| |
| /** |
| * Sends the {@code KeyEvent}, which is not consumed by the mozc server. |
| */ |
| @VisibleForTesting void sendKeyEvent(KeyEventInterface keyEvent) { |
| if (keyEvent == null) { |
| return; |
| } |
| |
| int keyCode = keyEvent.getKeyCode(); |
| // Some keys have a potential to be consumed from mozc client. |
| if (maybeProcessBackKey(keyCode) || maybeProcessActionKey(keyCode)) { |
| // The key event is consumed. |
| return; |
| } |
| |
| // Following code is to fallback to target activity. |
| Optional<KeyEvent> nativeKeyEvent = keyEvent.getNativeEvent(); |
| InputConnection inputConnection = getCurrentInputConnection(); |
| |
| if (nativeKeyEvent.isPresent() && inputConnection != null) { |
| // Meta keys are from this.onKeyDown/Up so fallback each time. |
| if (KeycodeConverter.isMetaKey(nativeKeyEvent.get())) { |
| inputConnection.sendKeyEvent(createKeyEvent( |
| nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), |
| nativeKeyEvent.get().getAction(), nativeKeyEvent.get().getRepeatCount())); |
| return; |
| } |
| |
| // Other keys are from this.onKeyDown so create dummy Down/Up events. |
| inputConnection.sendKeyEvent(createKeyEvent( |
| nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), KeyEvent.ACTION_DOWN, 0)); |
| |
| inputConnection.sendKeyEvent(createKeyEvent( |
| nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), KeyEvent.ACTION_UP, 0)); |
| return; |
| } |
| |
| // Otherwise, just delegates the key event to the connected application. |
| sendDownUpKeyEvents(keyCode); |
| } |
| |
| /** |
| * @return true if the key event is consumed |
| */ |
| private boolean maybeProcessBackKey(int keyCode) { |
| if (keyCode != KeyEvent.KEYCODE_BACK || !isInputViewShown()) { |
| return false; |
| } |
| |
| // Special handling for back key event, to close the software keyboard or its subview. |
| // First, try to hide the subview, such as the symbol input view or the cursor view. |
| // If neither is shown, hideSubInputView would fail, then hide the whole software keyboard. |
| if (!viewManager.hideSubInputView()) { |
| requestHideSelf(0); |
| } |
| return true; |
| } |
| |
| private boolean maybeProcessActionKey(int keyCode) { |
| // Handle the event iff the enter is pressed. |
| if (keyCode != KeyEvent.KEYCODE_ENTER || !isInputViewShown()) { |
| return false; |
| } |
| return sendEditorAction(true); |
| } |
| |
| /** |
| * Sends editor action to {@code InputConnection}. |
| * <p> |
| * The difference from {@link InputMethodService#sendDefaultEditorAction(boolean)} is |
| * that if custom action label is specified {@code EditorInfo#actionId} is sent instead. |
| */ |
| private boolean sendEditorAction(boolean fromEnterKey) { |
| // If custom action label is specified (=non-null), special action id is also specified. |
| // If there is no IME_FLAG_NO_ENTER_ACTION option, we should send the id to the InputConnection. |
| EditorInfo editorInfo = getCurrentInputEditorInfo(); |
| if (editorInfo != null |
| && (editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0 |
| && editorInfo.actionLabel != null) { |
| InputConnection inputConnection = getCurrentInputConnection(); |
| if (inputConnection != null) { |
| inputConnection.performEditorAction(editorInfo.actionId); |
| return true; |
| } |
| } |
| // No custom action label is specified. Fall back to default EditorAction. |
| return sendDefaultEditorAction(fromEnterKey); |
| } |
| |
| private static void maybeDeleteSurroundingText(Output output, InputConnection inputConnection) { |
| if (!output.hasDeletionRange()) { |
| return; |
| } |
| |
| DeletionRange range = output.getDeletionRange(); |
| int leftRange = -range.getOffset(); |
| int rightRange = range.getLength() - leftRange; |
| if (leftRange < 0 || rightRange < 0) { |
| // If the range does not include the current position, do nothing |
| // because Android's API does not expect such situation. |
| MozcLog.w("Deletion range has unsupported parameters: " + range.toString()); |
| return; |
| } |
| |
| if (!inputConnection.deleteSurroundingText(leftRange, rightRange)) { |
| MozcLog.e("Failed to delete surrounding text."); |
| } |
| } |
| |
| private static void maybeCommitText(Output output, InputConnection inputConnection) { |
| if (!output.hasResult()) { |
| return; |
| } |
| |
| String outputText = output.getResult().getValue(); |
| if (outputText.equals("")) { |
| // Do nothing for an empty result string. |
| return; |
| } |
| |
| int position = MozcUtil.CURSOR_POSITION_TAIL; |
| if (output.getResult().hasCursorOffset()) { |
| if (output.getResult().getCursorOffset() |
| == -outputText.codePointCount(0, outputText.length())) { |
| position = MozcUtil.CURSOR_POSITION_HEAD; |
| } else { |
| MozcLog.e("Unsupported position: " + output.getResult().toString()); |
| } |
| } |
| |
| if (!inputConnection.commitText(outputText, position)) { |
| MozcLog.e("Failed to commit text."); |
| } |
| } |
| |
| private void setComposingText(Command command, InputConnection inputConnection) { |
| Preconditions.checkNotNull(command); |
| Preconditions.checkNotNull(inputConnection); |
| |
| Output output = command.getOutput(); |
| if (!output.hasPreedit()) { |
| // If preedit field is empty, we should clear composing text in the InputConnection |
| // because Mozc server asks us to do so. |
| // But there is special situation in Android. |
| // On onWindowShown, SWITCH_INPUT_MODE command is sent as a step of initialization. |
| // In this case we reach here with empty preedit. |
| // As described above we should clear the composing text but if we do so |
| // texts in selection range (e.g., URL in OmniBox) is always cleared. |
| // To avoid from this issue, we don't clear the composing text if the input |
| // is SWITCH_INPUT_MODE. |
| Input input = command.getInput(); |
| if (input.getType() != Input.CommandType.SEND_COMMAND |
| || input.getCommand().getType() != SessionCommand.CommandType.SWITCH_INPUT_MODE) { |
| if (!inputConnection.setComposingText("", 0)) { |
| MozcLog.e("Failed to set composing text."); |
| } |
| } |
| return; |
| } |
| |
| // Builds preedit expression. |
| Preedit preedit = output.getPreedit(); |
| |
| SpannableStringBuilder builder = new SpannableStringBuilder(); |
| for (Segment segment : preedit.getSegmentList()) { |
| builder.append(segment.getValue()); |
| } |
| |
| // Set underline for all the preedit text. |
| builder.setSpan(SPAN_UNDERLINE, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); |
| |
| // Draw cursor if in composition mode. |
| int cursor = preedit.getCursor(); |
| int spanFlags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING; |
| if (output.hasAllCandidateWords() |
| && output.getAllCandidateWords().hasCategory() |
| && output.getAllCandidateWords().getCategory() == ProtoCandidates.Category.CONVERSION) { |
| int offsetInString = 0; |
| for (Segment segment : preedit.getSegmentList()) { |
| int length = segment.getValue().length(); |
| builder.setSpan( |
| segment.hasAnnotation() && segment.getAnnotation() == Annotation.HIGHLIGHT |
| ? SPAN_CONVERT_HIGHLIGHT |
| : CharacterStyle.class.cast(new BackgroundColorSpan(CONVERT_NORMAL_COLOR)), |
| offsetInString, offsetInString + length, spanFlags); |
| offsetInString += length; |
| } |
| } else { |
| // We cannot show system cursor inside preedit here. |
| // Instead we change text style before the preedit's cursor. |
| int cursorOffsetInString = builder.toString().offsetByCodePoints(0, cursor); |
| if (cursor != builder.length()) { |
| builder.setSpan(SPAN_PARTIAL_SUGGESTION_COLOR, cursorOffsetInString, builder.length(), |
| spanFlags); |
| } |
| if (cursor > 0) { |
| builder.setSpan(SPAN_BEFORE_CURSOR, 0, cursorOffsetInString, spanFlags); |
| } |
| } |
| |
| // System cursor will be moved to the tail of preedit. |
| // It triggers onUpdateSelection again. |
| int cursorPosition = cursor > 0 ? MozcUtil.CURSOR_POSITION_TAIL : 0; |
| if (!inputConnection.setComposingText(builder, cursorPosition)) { |
| MozcLog.e("Failed to set composing text."); |
| } |
| } |
| |
| private void maybeSetSelection(Output output, InputConnection inputConnection) { |
| if (!output.hasPreedit()) { |
| return; |
| } |
| |
| Preedit preedit = output.getPreedit(); |
| int cursor = preedit.getCursor(); |
| if (cursor == 0 || cursor == getPreeditLength(preedit)) { |
| // The cursor is at the beginning/ending of the preedit. So we don't anything about the |
| // caret setting. |
| return; |
| } |
| |
| int caretPosition = selectionTracker.getPreeditStartPosition(); |
| if (output.hasDeletionRange()) { |
| caretPosition += output.getDeletionRange().getOffset(); |
| } |
| if (output.hasResult()) { |
| caretPosition += output.getResult().getValue().length(); |
| } |
| if (output.hasPreedit()) { |
| caretPosition += output.getPreedit().getCursor(); |
| } |
| |
| if (!inputConnection.setSelection(caretPosition, caretPosition)) { |
| MozcLog.e("Failed to set selection."); |
| } |
| } |
| |
| private static int getPreeditLength(Preedit preedit) { |
| int result = 0; |
| for (int i = 0; i < preedit.getSegmentCount(); ++i) { |
| result += preedit.getSegment(i).getValueLength(); |
| } |
| return result; |
| } |
| |
| /** |
| * Propagates the preferences which affect client-side. |
| * |
| * If the previous parameter (this.clientSidePreference) is null, |
| * all the fields in the latest parameter are propagated. |
| * If not, only differences are propagated. |
| * |
| * After the execution, {@code this.propagatedClientSidePreference} is updated. |
| * |
| * @param newPreference the ClientSidePreference to be propagated |
| */ |
| @VisibleForTesting void propagateClientSidePreference(ClientSidePreference newPreference) { |
| // TODO(matsuzakit): Receive a Config to reflect the current device configuration. |
| if (newPreference == null) { |
| MozcLog.e("newPreference must be non-null. No update is performed."); |
| return; |
| } |
| ClientSidePreference oldPreference = propagatedClientSidePreference; |
| if (oldPreference == null |
| || oldPreference.isHapticFeedbackEnabled() != newPreference.isHapticFeedbackEnabled()) { |
| feedbackManager.setHapticFeedbackEnabled(newPreference.isHapticFeedbackEnabled()); |
| } |
| if (oldPreference == null |
| || oldPreference.getHapticFeedbackDuration() != newPreference.getHapticFeedbackDuration()) { |
| feedbackManager.setHapticFeedbackDuration(newPreference.getHapticFeedbackDuration()); |
| } |
| if (oldPreference == null |
| || oldPreference.isSoundFeedbackEnabled() != newPreference.isSoundFeedbackEnabled()) { |
| feedbackManager.setSoundFeedbackEnabled(newPreference.isSoundFeedbackEnabled()); |
| } |
| if (oldPreference == null |
| || oldPreference.getSoundFeedbackVolume() != newPreference.getSoundFeedbackVolume()) { |
| // The default value is 0.4f. In order to set the 50 to the default value, divide the |
| // preference value by 125f heuristically. |
| feedbackManager.setSoundFeedbackVolume(newPreference.getSoundFeedbackVolume() / 125f); |
| } |
| if (oldPreference == null |
| || oldPreference.isPopupFeedbackEnabled() != newPreference.isPopupFeedbackEnabled()) { |
| viewManager.setPopupEnabled(newPreference.isPopupFeedbackEnabled()); |
| } |
| if (oldPreference == null |
| || oldPreference.getKeyboardLayout() != newPreference.getKeyboardLayout()) { |
| viewManager.setKeyboardLayout(newPreference.getKeyboardLayout()); |
| } |
| if (oldPreference == null |
| || oldPreference.getInputStyle() != newPreference.getInputStyle()) { |
| viewManager.setInputStyle(newPreference.getInputStyle()); |
| } |
| if (oldPreference == null |
| || oldPreference.isQwertyLayoutForAlphabet() != newPreference.isQwertyLayoutForAlphabet()) { |
| viewManager.setQwertyLayoutForAlphabet(newPreference.isQwertyLayoutForAlphabet()); |
| } |
| if (oldPreference == null |
| || oldPreference.isFullscreenMode() != newPreference.isFullscreenMode()) { |
| viewManager.setFullscreenMode( |
| applicationCompatibility.isFullScreenModeSupported() && newPreference.isFullscreenMode()); |
| } |
| if (oldPreference == null |
| || oldPreference.getFlickSensitivity() != newPreference.getFlickSensitivity()) { |
| viewManager.setFlickSensitivity(newPreference.getFlickSensitivity()); |
| } |
| if (oldPreference == null |
| || oldPreference.getEmojiProviderType() != newPreference.getEmojiProviderType()) { |
| viewManager.setEmojiProviderType(newPreference.getEmojiProviderType()); |
| } |
| if (oldPreference == null |
| || oldPreference.getHardwareKeyMap() != newPreference.getHardwareKeyMap()) { |
| viewManager.setHardwareKeyMap(newPreference.getHardwareKeyMap()); |
| } |
| if (oldPreference == null |
| || oldPreference.getSkinType() != newPreference.getSkinType()) { |
| viewManager.setSkin(newPreference.getSkinType().getSkin(getResources())); |
| } |
| if (oldPreference == null |
| || oldPreference.isMicrophoneButtonEnabled() != newPreference.isMicrophoneButtonEnabled()) { |
| viewManager.setMicrophoneButtonEnabledByPreference(newPreference.isMicrophoneButtonEnabled()); |
| } |
| if (oldPreference == null |
| || oldPreference.getLayoutAdjustment() != newPreference.getLayoutAdjustment()) { |
| viewManager.setLayoutAdjustment(newPreference.getLayoutAdjustment()); |
| } |
| if (oldPreference == null |
| || oldPreference.getKeyboardHeightRatio() != newPreference.getKeyboardHeightRatio()) { |
| viewManager.setKeyboardHeightRatio(newPreference.getKeyboardHeightRatio()); |
| } |
| |
| propagatedClientSidePreference = newPreference; |
| } |
| |
| /** |
| * Sends imposed config to the Mozc server. |
| * |
| * Some config items should be mobile ones. |
| * For example, "selection shortcut" should be disabled on software keyboard |
| * regardless of stored config if there is no hardware keyboard. |
| */ |
| private void updateImposedConfig() { |
| // TODO(hsumita): Respect Config.SelectionShortcut. |
| SelectionShortcut shortcutMode = (viewManager != null && viewManager.isFloatingCandidateMode()) |
| ? SelectionShortcut.SHORTCUT_123456789 : SelectionShortcut.NO_SHORTCUT; |
| |
| // TODO(matsuzakit): deviceConfig should be used to set following config items. |
| sessionExecutor.setImposedConfig(Config.newBuilder() |
| .setSessionKeymap(SessionKeymap.MOBILE) |
| .setSelectionShortcut(shortcutMode) |
| .setUseEmojiConversion(true) |
| .build()); |
| } |
| |
| /** |
| * A call-back to catch all the change on any preferences. |
| */ |
| private class SharedPreferenceChangeAdapter implements OnSharedPreferenceChangeListener { |
| @Override |
| public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { |
| if (isDebugBuild) { |
| MozcLog.d("onSharedPreferenceChanged : " + key); |
| } |
| if (key.startsWith(PREF_TWEAK_PREFIX)) { |
| // If the key belongs to PREF_TWEAK group, re-create SessionHandler and view. |
| prepareEveryTime(sharedPreferences, getConfiguration()); |
| setInputView(onCreateInputView()); |
| return; |
| } |
| propagateClientSidePreference( |
| new ClientSidePreference( |
| sharedPreferences, getResources(), getConfiguration().orientation)); |
| sessionExecutor.setConfig(ConfigUtil.toConfig(sharedPreferences)); |
| sessionExecutor.preferenceUsageStatsEvent(sharedPreferences, getResources()); |
| } |
| } |
| |
| @VisibleForTesting void maybeSetNarrowMode(Configuration configuration) { |
| // If given hardKeyboardHidden is equal to current one, skip updating narrow mode. |
| // In other words, only hardKeyboardHidden flag changes narrow mode automatically. |
| // This behavior is beneficial for a user who want to change narrow/full mode manually |
| // because this method keeps current narrow mode unless hardware keyboard connection is changed. |
| if (viewManager != null && configuration.hardKeyboardHidden != currentHardKeyboardHidden) { |
| currentHardKeyboardHidden = configuration.hardKeyboardHidden; |
| switch (currentHardKeyboardHidden) { |
| case Configuration.HARDKEYBOARDHIDDEN_NO: |
| if (!viewManager.isNarrowMode()) { |
| viewManager.hideSubInputView(); |
| viewManager.setNarrowMode(true); |
| } |
| break; |
| case Configuration.HARDKEYBOARDHIDDEN_YES: |
| if (viewManager.isNarrowMode()) { |
| viewManager.setNarrowMode(false); |
| } |
| break; |
| case Configuration.HARDKEYBOARDHIDDEN_UNDEFINED: |
| break; |
| } |
| } |
| } |
| |
| @VisibleForTesting void onConfigurationChangedInternal(Configuration newConfig) { |
| InputConnection inputConnection = getCurrentInputConnection(); |
| if (inputConnection != null) { |
| if (inputBound) { |
| inputConnection.finishComposingText(); |
| } |
| int selectionStart = selectionTracker.getLastSelectionStart(); |
| int selectionEnd = selectionTracker.getLastSelectionEnd(); |
| if (selectionStart >= 0 && selectionEnd >= 0) { |
| // We need to keep the last caret position, but it will be soon overwritten in |
| // onStartInput. Theoretically, we should prohibit the overwriting, but unfortunately |
| // there is no good way to figure out whether the invocation of onStartInput is caused by |
| // configuration change, or not. Thus, instead, we'll make an event to invoke |
| // onUpdateSelectionInternal with an expected position after the onStartInput invocation, |
| // so that it will again overwrite the caret position. |
| // Note that, if a user rotates the device with holding preedit text, it will be committed |
| // by finishComposingText above, and onUpdateSelection will be invoked from the framework. |
| // Invoke onUpdateSelectionInternal twice with same arguments should be safe in this |
| // situation. |
| configurationChangedHandler.sendMessage( |
| configurationChangedHandler.obtainMessage(0, selectionStart, selectionEnd)); |
| } |
| } |
| resetContext(); |
| selectionTracker.onConfigurationChanged(); |
| |
| sessionExecutor.updateRequest( |
| MozcUtil.getRequestBuilder(getResources(), currentKeyboardSpecification, newConfig).build(), |
| Collections.<TouchEvent>emptyList()); |
| |
| // NOTE : This method is not called at the time when the service is started. |
| // Based on newConfig, client side preferences should be sent |
| // because they change based on device config. |
| propagateClientSidePreference(new ClientSidePreference( |
| Preconditions.checkNotNull(PreferenceManager.getDefaultSharedPreferences(this)), |
| getResources(), newConfig.orientation)); |
| maybeSetNarrowMode(newConfig); |
| viewManager.onConfigurationChanged(newConfig); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| onConfigurationChangedInternal(newConfig); |
| // super.onConfigurationChanged must be called after propagateClientSidePreference |
| // to use updated MobileConfiguration. |
| super.onConfigurationChanged(newConfig); |
| } |
| |
| @VisibleForTesting |
| void onUpdateSelectionInternal(int oldSelStart, int oldSelEnd, |
| int newSelStart, int newSelEnd, |
| int candidatesStart, int candidatesEnd) { |
| MozcLog.d("start MozcService#onUpdateSelectionInternal " + System.nanoTime()); |
| if (isDebugBuild) { |
| MozcLog.d("selection updated: [" + oldSelStart + ":" + oldSelEnd + "] " |
| + "to: [" + newSelStart + ":" + newSelEnd + "] " |
| + "candidates: [" + candidatesStart + ":" + candidatesEnd + "]"); |
| } |
| |
| int updateStatus = selectionTracker.onUpdateSelection( |
| oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); |
| if (isDebugBuild) { |
| MozcLog.d(selectionTracker.toString()); |
| } |
| switch (updateStatus) { |
| case SelectionTracker.DO_NOTHING: |
| // Do nothing. |
| break; |
| case SelectionTracker.RESET_CONTEXT: { |
| sessionExecutor.resetContext(); |
| |
| // Commit the current composing text (preedit text), in case we hit an unknown state. |
| // Keeping the composing text sometimes makes it impossible for users to input characters, |
| // because it can cause consecutive mis-understanding of caret positions. |
| // We do this iff the keyboard is shown, because some other application may edit |
| // composition string, such as Google Translate. |
| if (isInputViewShown() && inputBound) { |
| InputConnection inputConnection = getCurrentInputConnection(); |
| if (inputConnection != null) { |
| inputConnection.finishComposingText(); |
| } |
| } |
| |
| // Rendering default Command causes hiding candidate window, |
| // and re-showing the keyboard view. |
| viewManager.render(Command.getDefaultInstance()); |
| break; |
| } |
| default: |
| // Otherwise, the updateStatus is the position of the cursor to be moved. |
| if (updateStatus < 0) { |
| throw new AssertionError("Unknown update status: " + updateStatus); |
| } |
| sessionExecutor.moveCursor(updateStatus, renderResultCallback); |
| break; |
| } |
| |
| MozcLog.d("end MozcService#onUpdateSelectionInternal " + System.nanoTime()); |
| } |
| |
| @Override |
| public void onUpdateSelection(int oldSelStart, int oldSelEnd, |
| int newSelStart, int newSelEnd, |
| int candidatesStart, int candidatesEnd) { |
| onUpdateSelectionInternal( |
| oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); |
| super.onUpdateSelection( |
| oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); |
| } |
| |
| private void trimMemory() { |
| // We must guarantee the contract of MemoryManageable#trimMemory. |
| if (!isInputViewShown()) { |
| MozcLog.d("Trimming memory"); |
| sessionExecutor.deleteSession(); |
| viewManager.trimMemory(); |
| } |
| } |
| |
| // To use special Configuration for testing, overriding this method might works. |
| @VisibleForTesting |
| Configuration getConfiguration() { |
| return getResources().getConfiguration(); |
| } |
| |
| @VisibleForTesting |
| ViewEventListener getViewEventListener() { |
| return eventListener; |
| } |
| |
| @Override |
| @VisibleForTesting |
| public void attachBaseContext(Context base) { |
| super.attachBaseContext(base); |
| } |
| } |