/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.pinyin; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Configuration; import android.inputmethodservice.InputMethodService; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.preference.PreferenceManager; import android.util.Log; import android.view.Gravity; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.View.MeasureSpec; import android.view.ViewGroup.LayoutParams; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; import android.widget.PopupWindow; import java.util.ArrayList; import java.util.List; import java.util.Vector; /** * Main class of the Pinyin input method. */ public class PinyinIME extends InputMethodService { /** * TAG for debug. */ static final String TAG = "PinyinIME"; /** * If is is true, IME will simulate key events for delete key, and send the * events back to the application. */ private static final boolean SIMULATE_KEY_DELETE = true; /** * Necessary environment configurations like screen size for this IME. */ private Environment mEnvironment; /** * Used to switch input mode. */ private InputModeSwitcher mInputModeSwitcher; /** * Soft keyboard container view to host real soft keyboard view. */ private SkbContainer mSkbContainer; /** * The floating container which contains the composing view. If necessary, * some other view like candiates container can also be put here. */ private LinearLayout mFloatingContainer; /** * View to show the composing string. */ private ComposingView mComposingView; /** * Window to show the composing string. */ private PopupWindow mFloatingWindow; /** * Used to show the floating window. */ private PopupTimer mFloatingWindowTimer = new PopupTimer(); /** * View to show candidates list. */ private CandidatesContainer mCandidatesContainer; /** * Balloon used when user presses a candidate. */ private BalloonHint mCandidatesBalloon; /** * Used to notify the input method when the user touch a candidate. */ private ChoiceNotifier mChoiceNotifier; /** * Used to notify gestures from soft keyboard. */ private OnGestureListener mGestureListenerSkb; /** * Used to notify gestures from candidates view. */ private OnGestureListener mGestureListenerCandidates; /** * The on-screen movement gesture detector for soft keyboard. */ private GestureDetector mGestureDetectorSkb; /** * The on-screen movement gesture detector for candidates view. */ private GestureDetector mGestureDetectorCandidates; /** * Option dialog to choose settings and other IMEs. */ private AlertDialog mOptionsDialog; /** * Connection used to bind the decoding service. */ private PinyinDecoderServiceConnection mPinyinDecoderServiceConnection; /** * The current IME status. * * @see com.android.inputmethod.pinyin.PinyinIME.ImeState */ private ImeState mImeState = ImeState.STATE_IDLE; /** * The decoding information, include spelling(Pinyin) string, decoding * result, etc. */ private DecodingInfo mDecInfo = new DecodingInfo(); /** * For English input. */ private EnglishInputProcessor mImEn; // receive ringer mode changes private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { SoundManager.getInstance(context).updateRingerMode(); } }; @Override public void onCreate() { mEnvironment = Environment.getInstance(); if (mEnvironment.needDebug()) { Log.d(TAG, "onCreate."); } super.onCreate(); startPinyinDecoderService(); mImEn = new EnglishInputProcessor(); Settings.getInstance(PreferenceManager .getDefaultSharedPreferences(getApplicationContext())); mInputModeSwitcher = new InputModeSwitcher(this); mChoiceNotifier = new ChoiceNotifier(this); mGestureListenerSkb = new OnGestureListener(false); mGestureListenerCandidates = new OnGestureListener(true); mGestureDetectorSkb = new GestureDetector(this, mGestureListenerSkb); mGestureDetectorCandidates = new GestureDetector(this, mGestureListenerCandidates); mEnvironment.onConfigurationChanged(getResources().getConfiguration(), this); } @Override public void onDestroy() { if (mEnvironment.needDebug()) { Log.d(TAG, "onDestroy."); } unbindService(mPinyinDecoderServiceConnection); Settings.releaseInstance(); super.onDestroy(); } @Override public void onConfigurationChanged(Configuration newConfig) { Environment env = Environment.getInstance(); if (mEnvironment.needDebug()) { Log.d(TAG, "onConfigurationChanged"); Log.d(TAG, "--last config: " + env.getConfiguration().toString()); Log.d(TAG, "---new config: " + newConfig.toString()); } // We need to change the local environment first so that UI components // can get the environment instance to handle size issues. When // super.onConfigurationChanged() is called, onCreateCandidatesView() // and onCreateInputView() will be executed if necessary. env.onConfigurationChanged(newConfig, this); // Clear related UI of the previous configuration. if (null != mSkbContainer) { mSkbContainer.dismissPopups(); } if (null != mCandidatesBalloon) { mCandidatesBalloon.dismiss(); } super.onConfigurationChanged(newConfig); resetToIdleState(false); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (processKey(event, 0 != event.getRepeatCount())) return true; return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (processKey(event, true)) return true; return super.onKeyUp(keyCode, event); } private boolean processKey(KeyEvent event, boolean realAction) { if (ImeState.STATE_BYPASS == mImeState) return false; int keyCode = event.getKeyCode(); // SHIFT-SPACE is used to switch between Chinese and English // when HKB is on. if (KeyEvent.KEYCODE_SPACE == keyCode && event.isShiftPressed()) { if (!realAction) return true; updateIcon(mInputModeSwitcher.switchLanguageWithHkb()); resetToIdleState(false); int allMetaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON | KeyEvent.META_SYM_ON; getCurrentInputConnection().clearMetaKeyStates(allMetaState); return true; } // If HKB is on to input English, by-pass the key event so that // default key listener will handle it. if (mInputModeSwitcher.isEnglishWithHkb()) { return false; } if (processFunctionKeys(keyCode, realAction)) { return true; } int keyChar = 0; if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { keyChar = keyCode - KeyEvent.KEYCODE_A + 'a'; } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { keyChar = keyCode - KeyEvent.KEYCODE_0 + '0'; } else if (keyCode == KeyEvent.KEYCODE_COMMA) { keyChar = ','; } else if (keyCode == KeyEvent.KEYCODE_PERIOD) { keyChar = '.'; } else if (keyCode == KeyEvent.KEYCODE_SPACE) { keyChar = ' '; } else if (keyCode == KeyEvent.KEYCODE_APOSTROPHE) { keyChar = '\''; } if (mInputModeSwitcher.isEnglishWithSkb()) { return mImEn.processKey(getCurrentInputConnection(), event, mInputModeSwitcher.isEnglishUpperCaseWithSkb(), realAction); } else if (mInputModeSwitcher.isChineseText()) { if (mImeState == ImeState.STATE_IDLE || mImeState == ImeState.STATE_APP_COMPLETION) { mImeState = ImeState.STATE_IDLE; return processStateIdle(keyChar, keyCode, event, realAction); } else if (mImeState == ImeState.STATE_INPUT) { return processStateInput(keyChar, keyCode, event, realAction); } else if (mImeState == ImeState.STATE_PREDICT) { return processStatePredict(keyChar, keyCode, event, realAction); } else if (mImeState == ImeState.STATE_COMPOSING) { return processStateEditComposing(keyChar, keyCode, event, realAction); } } else { if (0 != keyChar && realAction) { commitResultText(String.valueOf((char) keyChar)); } } return false; } // keyCode can be from both hard key or soft key. private boolean processFunctionKeys(int keyCode, boolean realAction) { // Back key is used to dismiss all popup UI in a soft keyboard. if (keyCode == KeyEvent.KEYCODE_BACK) { if (isInputViewShown()) { if (mSkbContainer.handleBack(realAction)) return true; } } // Chinese related input is handle separately. if (mInputModeSwitcher.isChineseText()) { return false; } if (null != mCandidatesContainer && mCandidatesContainer.isShown() && !mDecInfo.isCandidatesListEmpty()) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { if (!realAction) return true; chooseCandidate(-1); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { if (!realAction) return true; mCandidatesContainer.activeCurseBackward(); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { if (!realAction) return true; mCandidatesContainer.activeCurseForward(); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { if (!realAction) return true; mCandidatesContainer.pageBackward(false, true); return true; } if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { if (!realAction) return true; mCandidatesContainer.pageForward(false, true); return true; } if (keyCode == KeyEvent.KEYCODE_DEL && ImeState.STATE_PREDICT == mImeState) { if (!realAction) return true; resetToIdleState(false); return true; } } else { if (keyCode == KeyEvent.KEYCODE_DEL) { if (!realAction) return true; if (SIMULATE_KEY_DELETE) { simulateKeyEventDownUp(keyCode); } else { getCurrentInputConnection().deleteSurroundingText(1, 0); } return true; } if (keyCode == KeyEvent.KEYCODE_ENTER) { if (!realAction) return true; sendKeyChar('\n'); return true; } if (keyCode == KeyEvent.KEYCODE_SPACE) { if (!realAction) return true; sendKeyChar(' '); return true; } } return false; } private boolean processStateIdle(int keyChar, int keyCode, KeyEvent event, boolean realAction) { // In this status, when user presses keys in [a..z], the status will // change to input state. if (keyChar >= 'a' && keyChar <= 'z' && !event.isAltPressed()) { if (!realAction) return true; mDecInfo.addSplChar((char) keyChar, true); chooseAndUpdate(-1); return true; } else if (keyCode == KeyEvent.KEYCODE_DEL) { if (!realAction) return true; if (SIMULATE_KEY_DELETE) { simulateKeyEventDownUp(keyCode); } else { getCurrentInputConnection().deleteSurroundingText(1, 0); } return true; } else if (keyCode == KeyEvent.KEYCODE_ENTER) { if (!realAction) return true; sendKeyChar('\n'); return true; } else if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { return true; } else if (event.isAltPressed()) { char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); if (0 != fullwidth_char) { if (realAction) { String result = String.valueOf(fullwidth_char); commitResultText(result); } return true; } else { if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { return true; } } } else if (keyChar != 0 && keyChar != '\t') { if (realAction) { if (keyChar == ',' || keyChar == '.') { inputCommaPeriod("", keyChar, false, ImeState.STATE_IDLE); } else { if (0 != keyChar) { String result = String.valueOf((char) keyChar); commitResultText(result); } } } return true; } return false; } private boolean processStateInput(int keyChar, int keyCode, KeyEvent event, boolean realAction) { // If ALT key is pressed, input alternative key. But if the // alternative key is quote key, it will be used for input a splitter // in Pinyin string. if (event.isAltPressed()) { if ('\'' != event.getUnicodeChar(event.getMetaState())) { if (realAction) { char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); if (0 != fullwidth_char) { commitResultText(mDecInfo .getCurrentFullSent(mCandidatesContainer .getActiveCandiatePos()) + String.valueOf(fullwidth_char)); resetToIdleState(false); } } return true; } else { keyChar = '\''; } } if (keyChar >= 'a' && keyChar <= 'z' || keyChar == '\'' && !mDecInfo.charBeforeCursorIsSeparator() || keyCode == KeyEvent.KEYCODE_DEL) { if (!realAction) return true; return processSurfaceChange(keyChar, keyCode); } else if (keyChar == ',' || keyChar == '.') { if (!realAction) return true; inputCommaPeriod(mDecInfo.getCurrentFullSent(mCandidatesContainer .getActiveCandiatePos()), keyChar, true, ImeState.STATE_IDLE); return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { if (!realAction) return true; if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { mCandidatesContainer.activeCurseBackward(); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mCandidatesContainer.activeCurseForward(); } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { // If it has been the first page, a up key will shift // the state to edit composing string. if (!mCandidatesContainer.pageBackward(false, true)) { mCandidatesContainer.enableActiveHighlight(false); changeToStateComposing(true); updateComposingText(true); } } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { mCandidatesContainer.pageForward(false, true); } return true; } else if (keyCode >= KeyEvent.KEYCODE_1 && keyCode <= KeyEvent.KEYCODE_9) { if (!realAction) return true; int activePos = keyCode - KeyEvent.KEYCODE_1; int currentPage = mCandidatesContainer.getCurrentPage(); if (activePos < mDecInfo.getCurrentPageSize(currentPage)) { activePos = activePos + mDecInfo.getCurrentPageStart(currentPage); if (activePos >= 0) { chooseAndUpdate(activePos); } } return true; } else if (keyCode == KeyEvent.KEYCODE_ENTER) { if (!realAction) return true; if (mInputModeSwitcher.isEnterNoramlState()) { commitResultText(mDecInfo.getOrigianlSplStr().toString()); resetToIdleState(false); } else { commitResultText(mDecInfo .getCurrentFullSent(mCandidatesContainer .getActiveCandiatePos())); sendKeyChar('\n'); resetToIdleState(false); } return true; } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE) { if (!realAction) return true; chooseCandidate(-1); return true; } else if (keyCode == KeyEvent.KEYCODE_BACK) { if (!realAction) return true; resetToIdleState(false); requestHideSelf(0); return true; } return false; } private boolean processStatePredict(int keyChar, int keyCode, KeyEvent event, boolean realAction) { if (!realAction) return true; // If ALT key is pressed, input alternative key. if (event.isAltPressed()) { char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); if (0 != fullwidth_char) { commitResultText(mDecInfo.getCandidate(mCandidatesContainer .getActiveCandiatePos()) + String.valueOf(fullwidth_char)); resetToIdleState(false); } return true; } // In this status, when user presses keys in [a..z], the status will // change to input state. if (keyChar >= 'a' && keyChar <= 'z') { changeToStateInput(true); mDecInfo.addSplChar((char) keyChar, true); chooseAndUpdate(-1); } else if (keyChar == ',' || keyChar == '.') { inputCommaPeriod("", keyChar, true, ImeState.STATE_IDLE); } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { mCandidatesContainer.activeCurseBackward(); } if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mCandidatesContainer.activeCurseForward(); } if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { mCandidatesContainer.pageBackward(false, true); } if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { mCandidatesContainer.pageForward(false, true); } } else if (keyCode == KeyEvent.KEYCODE_DEL) { resetToIdleState(false); } else if (keyCode == KeyEvent.KEYCODE_BACK) { resetToIdleState(false); requestHideSelf(0); } else if (keyCode >= KeyEvent.KEYCODE_1 && keyCode <= KeyEvent.KEYCODE_9) { int activePos = keyCode - KeyEvent.KEYCODE_1; int currentPage = mCandidatesContainer.getCurrentPage(); if (activePos < mDecInfo.getCurrentPageSize(currentPage)) { activePos = activePos + mDecInfo.getCurrentPageStart(currentPage); if (activePos >= 0) { chooseAndUpdate(activePos); } } } else if (keyCode == KeyEvent.KEYCODE_ENTER) { sendKeyChar('\n'); resetToIdleState(false); } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE) { chooseCandidate(-1); } return true; } private boolean processStateEditComposing(int keyChar, int keyCode, KeyEvent event, boolean realAction) { if (!realAction) return true; ComposingView.ComposingStatus cmpsvStatus = mComposingView.getComposingStatus(); // If ALT key is pressed, input alternative key. But if the // alternative key is quote key, it will be used for input a splitter // in Pinyin string. if (event.isAltPressed()) { if ('\'' != event.getUnicodeChar(event.getMetaState())) { char fullwidth_char = KeyMapDream.getChineseLabel(keyCode); if (0 != fullwidth_char) { String retStr; if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == cmpsvStatus) { retStr = mDecInfo.getOrigianlSplStr().toString(); } else { retStr = mDecInfo.getComposingStr(); } commitResultText(retStr + String.valueOf(fullwidth_char)); resetToIdleState(false); } return true; } else { keyChar = '\''; } } if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { if (!mDecInfo.selectionFinished()) { changeToStateInput(true); } } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { mComposingView.moveCursor(keyCode); } else if ((keyCode == KeyEvent.KEYCODE_ENTER && mInputModeSwitcher .isEnterNoramlState()) || keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE) { if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == cmpsvStatus) { String str = mDecInfo.getOrigianlSplStr().toString(); if (!tryInputRawUnicode(str)) { commitResultText(str); } } else if (ComposingView.ComposingStatus.EDIT_PINYIN == cmpsvStatus) { String str = mDecInfo.getComposingStr(); if (!tryInputRawUnicode(str)) { commitResultText(str); } } else { commitResultText(mDecInfo.getComposingStr()); } resetToIdleState(false); } else if (keyCode == KeyEvent.KEYCODE_ENTER && !mInputModeSwitcher.isEnterNoramlState()) { String retStr; if (!mDecInfo.isCandidatesListEmpty()) { retStr = mDecInfo.getCurrentFullSent(mCandidatesContainer .getActiveCandiatePos()); } else { retStr = mDecInfo.getComposingStr(); } commitResultText(retStr); sendKeyChar('\n'); resetToIdleState(false); } else if (keyCode == KeyEvent.KEYCODE_BACK) { resetToIdleState(false); requestHideSelf(0); return true; } else { return processSurfaceChange(keyChar, keyCode); } return true; } private boolean tryInputRawUnicode(String str) { if (str.length() > 7) { if (str.substring(0, 7).compareTo("unicode") == 0) { try { String digitStr = str.substring(7); int startPos = 0; int radix = 10; if (digitStr.length() > 2 && digitStr.charAt(0) == '0' && digitStr.charAt(1) == 'x') { startPos = 2; radix = 16; } digitStr = digitStr.substring(startPos); int unicode = Integer.parseInt(digitStr, radix); if (unicode > 0) { char low = (char) (unicode & 0x0000ffff); char high = (char) ((unicode & 0xffff0000) >> 16); commitResultText(String.valueOf(low)); if (0 != high) { commitResultText(String.valueOf(high)); } } return true; } catch (NumberFormatException e) { return false; } } else if (str.substring(str.length() - 7, str.length()).compareTo( "unicode") == 0) { String resultStr = ""; for (int pos = 0; pos < str.length() - 7; pos++) { if (pos > 0) { resultStr += " "; } resultStr += "0x" + Integer.toHexString(str.charAt(pos)); } commitResultText(String.valueOf(resultStr)); return true; } } return false; } private boolean processSurfaceChange(int keyChar, int keyCode) { if (mDecInfo.isSplStrFull() && KeyEvent.KEYCODE_DEL != keyCode) { return true; } if ((keyChar >= 'a' && keyChar <= 'z') || (keyChar == '\'' && !mDecInfo.charBeforeCursorIsSeparator()) || (((keyChar >= '0' && keyChar <= '9') || keyChar == ' ') && ImeState.STATE_COMPOSING == mImeState)) { mDecInfo.addSplChar((char) keyChar, false); chooseAndUpdate(-1); } else if (keyCode == KeyEvent.KEYCODE_DEL) { mDecInfo.prepareDeleteBeforeCursor(); chooseAndUpdate(-1); } return true; } private void changeToStateComposing(boolean updateUi) { mImeState = ImeState.STATE_COMPOSING; if (!updateUi) return; if (null != mSkbContainer && mSkbContainer.isShown()) { mSkbContainer.toggleCandidateMode(true); } } private void changeToStateInput(boolean updateUi) { mImeState = ImeState.STATE_INPUT; if (!updateUi) return; if (null != mSkbContainer && mSkbContainer.isShown()) { mSkbContainer.toggleCandidateMode(true); } showCandidateWindow(true); } private void simulateKeyEventDownUp(int keyCode) { InputConnection ic = getCurrentInputConnection(); if (null == ic) return; ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); } private void commitResultText(String resultText) { InputConnection ic = getCurrentInputConnection(); if (null != ic) ic.commitText(resultText, 1); if (null != mComposingView) { mComposingView.setVisibility(View.INVISIBLE); mComposingView.invalidate(); } } private void updateComposingText(boolean visible) { if (!visible) { mComposingView.setVisibility(View.INVISIBLE); } else { mComposingView.setDecodingInfo(mDecInfo, mImeState); mComposingView.setVisibility(View.VISIBLE); } mComposingView.invalidate(); } private void inputCommaPeriod(String preEdit, int keyChar, boolean dismissCandWindow, ImeState nextState) { if (keyChar == ',') preEdit += '\uff0c'; else if (keyChar == '.') preEdit += '\u3002'; else return; commitResultText(preEdit); if (dismissCandWindow) resetCandidateWindow(); mImeState = nextState; } private void resetToIdleState(boolean resetInlineText) { if (ImeState.STATE_IDLE == mImeState) return; mImeState = ImeState.STATE_IDLE; mDecInfo.reset(); if (null != mComposingView) mComposingView.reset(); if (resetInlineText) commitResultText(""); resetCandidateWindow(); } private void chooseAndUpdate(int candId) { if (!mInputModeSwitcher.isChineseText()) { String choice = mDecInfo.getCandidate(candId); if (null != choice) { commitResultText(choice); } resetToIdleState(false); return; } if (ImeState.STATE_PREDICT != mImeState) { // Get result candidate list, if choice_id < 0, do a new decoding. // If choice_id >=0, select the candidate, and get the new candidate // list. mDecInfo.chooseDecodingCandidate(candId); } else { // Choose a prediction item. mDecInfo.choosePredictChoice(candId); } if (mDecInfo.getComposingStr().length() > 0) { String resultStr; resultStr = mDecInfo.getComposingStrActivePart(); // choiceId >= 0 means user finishes a choice selection. if (candId >= 0 && mDecInfo.canDoPrediction()) { commitResultText(resultStr); mImeState = ImeState.STATE_PREDICT; if (null != mSkbContainer && mSkbContainer.isShown()) { mSkbContainer.toggleCandidateMode(false); } // Try to get the prediction list. if (Settings.getPrediction()) { InputConnection ic = getCurrentInputConnection(); if (null != ic) { CharSequence cs = ic.getTextBeforeCursor(3, 0); if (null != cs) { mDecInfo.preparePredicts(cs); } } } else { mDecInfo.resetCandidates(); } if (mDecInfo.mCandidatesList.size() > 0) { showCandidateWindow(false); } else { resetToIdleState(false); } } else { if (ImeState.STATE_IDLE == mImeState) { if (mDecInfo.getSplStrDecodedLen() == 0) { changeToStateComposing(true); } else { changeToStateInput(true); } } else { if (mDecInfo.selectionFinished()) { changeToStateComposing(true); } } showCandidateWindow(true); } } else { resetToIdleState(false); } } // If activeCandNo is less than 0, get the current active candidate number // from candidate view, otherwise use activeCandNo. private void chooseCandidate(int activeCandNo) { if (activeCandNo < 0) { activeCandNo = mCandidatesContainer.getActiveCandiatePos(); } if (activeCandNo >= 0) { chooseAndUpdate(activeCandNo); } } private boolean startPinyinDecoderService() { if (null == mDecInfo.mIPinyinDecoderService) { Intent serviceIntent = new Intent(); serviceIntent.setClass(this, PinyinDecoderService.class); if (null == mPinyinDecoderServiceConnection) { mPinyinDecoderServiceConnection = new PinyinDecoderServiceConnection(); } // Bind service if (bindService(serviceIntent, mPinyinDecoderServiceConnection, Context.BIND_AUTO_CREATE)) { return true; } else { return false; } } return true; } @Override public View onCreateCandidatesView() { if (mEnvironment.needDebug()) { Log.d(TAG, "onCreateCandidatesView."); } LayoutInflater inflater = getLayoutInflater(); // Inflate the floating container view mFloatingContainer = (LinearLayout) inflater.inflate( R.layout.floating_container, null); // The first child is the composing view. mComposingView = (ComposingView) mFloatingContainer.getChildAt(0); mCandidatesContainer = (CandidatesContainer) inflater.inflate( R.layout.candidates_container, null); // Create balloon hint for candidates view. mCandidatesBalloon = new BalloonHint(this, mCandidatesContainer, MeasureSpec.UNSPECIFIED); mCandidatesBalloon.setBalloonBackground(getResources().getDrawable( R.drawable.candidate_balloon_bg)); mCandidatesContainer.initialize(mChoiceNotifier, mCandidatesBalloon, mGestureDetectorCandidates); // The floating window if (null != mFloatingWindow && mFloatingWindow.isShowing()) { mFloatingWindowTimer.cancelShowing(); mFloatingWindow.dismiss(); } mFloatingWindow = new PopupWindow(this); mFloatingWindow.setClippingEnabled(false); mFloatingWindow.setBackgroundDrawable(null); mFloatingWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); mFloatingWindow.setContentView(mFloatingContainer); setCandidatesViewShown(true); return mCandidatesContainer; } public void responseSoftKeyEvent(SoftKey sKey) { if (null == sKey) return; InputConnection ic = getCurrentInputConnection(); if (ic == null) return; int keyCode = sKey.getKeyCode(); // Process some general keys, including KEYCODE_DEL, KEYCODE_SPACE, // KEYCODE_ENTER and KEYCODE_DPAD_CENTER. if (sKey.isKeyCodeKey()) { if (processFunctionKeys(keyCode, true)) return; } if (sKey.isUserDefKey()) { updateIcon(mInputModeSwitcher.switchModeForUserKey(keyCode)); resetToIdleState(false); mSkbContainer.updateInputMode(); } else { if (sKey.isKeyCodeKey()) { KeyEvent eDown = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD); KeyEvent eUp = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD); onKeyDown(keyCode, eDown); onKeyUp(keyCode, eUp); } else if (sKey.isUniStrKey()) { boolean kUsed = false; String keyLabel = sKey.getKeyLabel(); if (mInputModeSwitcher.isChineseTextWithSkb() && (ImeState.STATE_INPUT == mImeState || ImeState.STATE_COMPOSING == mImeState)) { if (mDecInfo.length() > 0 && keyLabel.length() == 1 && keyLabel.charAt(0) == '\'') { processSurfaceChange('\'', 0); kUsed = true; } } if (!kUsed) { if (ImeState.STATE_INPUT == mImeState) { commitResultText(mDecInfo .getCurrentFullSent(mCandidatesContainer .getActiveCandiatePos())); } else if (ImeState.STATE_COMPOSING == mImeState) { commitResultText(mDecInfo.getComposingStr()); } commitResultText(keyLabel); resetToIdleState(false); } } // If the current soft keyboard is not sticky, IME needs to go // back to the previous soft keyboard automatically. if (!mSkbContainer.isCurrentSkbSticky()) { updateIcon(mInputModeSwitcher.requestBackToPreviousSkb()); resetToIdleState(false); mSkbContainer.updateInputMode(); } } } private void showCandidateWindow(boolean showComposingView) { if (mEnvironment.needDebug()) { Log.d(TAG, "Candidates window is shown. Parent = " + mCandidatesContainer); } setCandidatesViewShown(true); if (null != mSkbContainer) mSkbContainer.requestLayout(); if (null == mCandidatesContainer) { resetToIdleState(false); return; } updateComposingText(showComposingView); mCandidatesContainer.showCandidates(mDecInfo, ImeState.STATE_COMPOSING != mImeState); mFloatingWindowTimer.postShowFloatingWindow(); } private void dismissCandidateWindow() { if (mEnvironment.needDebug()) { Log.d(TAG, "Candidates window is to be dismissed"); } if (null == mCandidatesContainer) return; try { mFloatingWindowTimer.cancelShowing(); mFloatingWindow.dismiss(); } catch (Exception e) { Log.e(TAG, "Fail to show the PopupWindow."); } setCandidatesViewShown(false); if (null != mSkbContainer && mSkbContainer.isShown()) { mSkbContainer.toggleCandidateMode(false); } } private void resetCandidateWindow() { if (mEnvironment.needDebug()) { Log.d(TAG, "Candidates window is to be reset"); } if (null == mCandidatesContainer) return; try { mFloatingWindowTimer.cancelShowing(); mFloatingWindow.dismiss(); } catch (Exception e) { Log.e(TAG, "Fail to show the PopupWindow."); } if (null != mSkbContainer && mSkbContainer.isShown()) { mSkbContainer.toggleCandidateMode(false); } mDecInfo.resetCandidates(); if (null != mCandidatesContainer && mCandidatesContainer.isShown()) { showCandidateWindow(false); } } private void updateIcon(int iconId) { if (iconId > 0) { showStatusIcon(iconId); } else { hideStatusIcon(); } } @Override public View onCreateInputView() { if (mEnvironment.needDebug()) { Log.d(TAG, "onCreateInputView."); } LayoutInflater inflater = getLayoutInflater(); mSkbContainer = (SkbContainer) inflater.inflate(R.layout.skb_container, null); mSkbContainer.setService(this); mSkbContainer.setInputModeSwitcher(mInputModeSwitcher); mSkbContainer.setGestureDetector(mGestureDetectorSkb); return mSkbContainer; } @Override public void onStartInput(EditorInfo editorInfo, boolean restarting) { if (mEnvironment.needDebug()) { Log.d(TAG, "onStartInput " + " ccontentType: " + String.valueOf(editorInfo.inputType) + " Restarting:" + String.valueOf(restarting)); } updateIcon(mInputModeSwitcher.requestInputWithHkb(editorInfo)); resetToIdleState(false); } @Override public void onStartInputView(EditorInfo editorInfo, boolean restarting) { if (mEnvironment.needDebug()) { Log.d(TAG, "onStartInputView " + " contentType: " + String.valueOf(editorInfo.inputType) + " Restarting:" + String.valueOf(restarting)); } updateIcon(mInputModeSwitcher.requestInputWithSkb(editorInfo)); resetToIdleState(false); mSkbContainer.updateInputMode(); setCandidatesViewShown(false); } @Override public void onFinishInputView(boolean finishingInput) { if (mEnvironment.needDebug()) { Log.d(TAG, "onFinishInputView."); } resetToIdleState(false); super.onFinishInputView(finishingInput); } @Override public void onFinishInput() { if (mEnvironment.needDebug()) { Log.d(TAG, "onFinishInput."); } resetToIdleState(false); super.onFinishInput(); } @Override public void onFinishCandidatesView(boolean finishingInput) { if (mEnvironment.needDebug()) { Log.d(TAG, "onFinishCandidateView."); } resetToIdleState(false); super.onFinishCandidatesView(finishingInput); } @Override public void onDisplayCompletions(CompletionInfo[] completions) { if (!isFullscreenMode()) return; if (null == completions || completions.length <= 0) return; if (null == mSkbContainer || !mSkbContainer.isShown()) return; if (!mInputModeSwitcher.isChineseText() || ImeState.STATE_IDLE == mImeState || ImeState.STATE_PREDICT == mImeState) { mImeState = ImeState.STATE_APP_COMPLETION; mDecInfo.prepareAppCompletions(completions); showCandidateWindow(false); } } private void onChoiceTouched(int activeCandNo) { if (mImeState == ImeState.STATE_COMPOSING) { changeToStateInput(true); } else if (mImeState == ImeState.STATE_INPUT || mImeState == ImeState.STATE_PREDICT) { chooseCandidate(activeCandNo); } else if (mImeState == ImeState.STATE_APP_COMPLETION) { if (null != mDecInfo.mAppCompletions && activeCandNo >= 0 && activeCandNo < mDecInfo.mAppCompletions.length) { CompletionInfo ci = mDecInfo.mAppCompletions[activeCandNo]; if (null != ci) { InputConnection ic = getCurrentInputConnection(); ic.commitCompletion(ci); } } resetToIdleState(false); } } @Override public void requestHideSelf(int flags) { if (mEnvironment.needDebug()) { Log.d(TAG, "DimissSoftInput."); } dismissCandidateWindow(); if (null != mSkbContainer && mSkbContainer.isShown()) { mSkbContainer.dismissPopups(); } super.requestHideSelf(flags); } public void showOptionsMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.app_icon); builder.setNegativeButton(android.R.string.cancel, null); CharSequence itemSettings = getString(R.string.ime_settings_activity_name); CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod); builder.setItems(new CharSequence[] {itemSettings, itemInputMethod}, new DialogInterface.OnClickListener() { public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case 0: launchSettings(); break; case 1: InputMethodManager.getInstance() .showInputMethodPicker(); break; } } }); builder.setTitle(getString(R.string.ime_name)); mOptionsDialog = builder.create(); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); lp.token = mSkbContainer.getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); mOptionsDialog.show(); } private void launchSettings() { Intent intent = new Intent(); intent.setClass(PinyinIME.this, SettingsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } private class PopupTimer extends Handler implements Runnable { private int mParentLocation[] = new int[2]; void postShowFloatingWindow() { mFloatingContainer.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); mFloatingWindow.setWidth(mFloatingContainer.getMeasuredWidth()); mFloatingWindow.setHeight(mFloatingContainer.getMeasuredHeight()); post(this); } void cancelShowing() { if (mFloatingWindow.isShowing()) { mFloatingWindow.dismiss(); } removeCallbacks(this); } public void run() { mCandidatesContainer.getLocationInWindow(mParentLocation); if (!mFloatingWindow.isShowing()) { mFloatingWindow.showAtLocation(mCandidatesContainer, Gravity.LEFT | Gravity.TOP, mParentLocation[0], mParentLocation[1] -mFloatingWindow.getHeight()); } else { mFloatingWindow .update(mParentLocation[0], mParentLocation[1] - mFloatingWindow.getHeight(), mFloatingWindow.getWidth(), mFloatingWindow.getHeight()); } } } /** * Used to notify IME that the user selects a candidate or performs an * gesture. */ public class ChoiceNotifier extends Handler implements CandidateViewListener { PinyinIME mIme; ChoiceNotifier(PinyinIME ime) { mIme = ime; } public void onClickChoice(int choiceId) { if (choiceId >= 0) { mIme.onChoiceTouched(choiceId); } } public void onToLeftGesture() { if (ImeState.STATE_COMPOSING == mImeState) { changeToStateInput(true); } mCandidatesContainer.pageForward(true, false); } public void onToRightGesture() { if (ImeState.STATE_COMPOSING == mImeState) { changeToStateInput(true); } mCandidatesContainer.pageBackward(true, false); } public void onToTopGesture() { } public void onToBottomGesture() { } } public class OnGestureListener extends GestureDetector.SimpleOnGestureListener { /** * When user presses and drags, the minimum x-distance to make a * response to the drag event. */ private static final int MIN_X_FOR_DRAG = 60; /** * When user presses and drags, the minimum y-distance to make a * response to the drag event. */ private static final int MIN_Y_FOR_DRAG = 40; /** * Velocity threshold for a screen-move gesture. If the minimum * x-velocity is less than it, no gesture. */ static private final float VELOCITY_THRESHOLD_X1 = 0.3f; /** * Velocity threshold for a screen-move gesture. If the maximum * x-velocity is less than it, no gesture. */ static private final float VELOCITY_THRESHOLD_X2 = 0.7f; /** * Velocity threshold for a screen-move gesture. If the minimum * y-velocity is less than it, no gesture. */ static private final float VELOCITY_THRESHOLD_Y1 = 0.2f; /** * Velocity threshold for a screen-move gesture. If the maximum * y-velocity is less than it, no gesture. */ static private final float VELOCITY_THRESHOLD_Y2 = 0.45f; /** If it false, we will not response detected gestures. */ private boolean mReponseGestures; /** The minimum X velocity observed in the gesture. */ private float mMinVelocityX = Float.MAX_VALUE; /** The minimum Y velocity observed in the gesture. */ private float mMinVelocityY = Float.MAX_VALUE; /** The first down time for the series of touch events for an action. */ private long mTimeDown; /** The last time when onScroll() is called. */ private long mTimeLastOnScroll; /** This flag used to indicate that this gesture is not a gesture. */ private boolean mNotGesture; /** This flag used to indicate that this gesture has been recognized. */ private boolean mGestureRecognized; public OnGestureListener(boolean reponseGestures) { mReponseGestures = reponseGestures; } @Override public boolean onDown(MotionEvent e) { mMinVelocityX = Integer.MAX_VALUE; mMinVelocityY = Integer.MAX_VALUE; mTimeDown = e.getEventTime(); mTimeLastOnScroll = mTimeDown; mNotGesture = false; mGestureRecognized = false; return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (mNotGesture) return false; if (mGestureRecognized) return true; if (Math.abs(e1.getX() - e2.getX()) < MIN_X_FOR_DRAG && Math.abs(e1.getY() - e2.getY()) < MIN_Y_FOR_DRAG) return false; long timeNow = e2.getEventTime(); long spanTotal = timeNow - mTimeDown; long spanThis = timeNow - mTimeLastOnScroll; if (0 == spanTotal) spanTotal = 1; if (0 == spanThis) spanThis = 1; float vXTotal = (e2.getX() - e1.getX()) / spanTotal; float vYTotal = (e2.getY() - e1.getY()) / spanTotal; // The distances are from the current point to the previous one. float vXThis = -distanceX / spanThis; float vYThis = -distanceY / spanThis; float kX = vXTotal * vXThis; float kY = vYTotal * vYThis; float k1 = kX + kY; float k2 = Math.abs(kX) + Math.abs(kY); if (k1 / k2 < 0.8) { mNotGesture = true; return false; } float absVXTotal = Math.abs(vXTotal); float absVYTotal = Math.abs(vYTotal); if (absVXTotal < mMinVelocityX) { mMinVelocityX = absVXTotal; } if (absVYTotal < mMinVelocityY) { mMinVelocityY = absVYTotal; } if (mMinVelocityX < VELOCITY_THRESHOLD_X1 && mMinVelocityY < VELOCITY_THRESHOLD_Y1) { mNotGesture = true; return false; } if (vXTotal > VELOCITY_THRESHOLD_X2 && absVYTotal < VELOCITY_THRESHOLD_Y2) { if (mReponseGestures) onDirectionGesture(Gravity.RIGHT); mGestureRecognized = true; } else if (vXTotal < -VELOCITY_THRESHOLD_X2 && absVYTotal < VELOCITY_THRESHOLD_Y2) { if (mReponseGestures) onDirectionGesture(Gravity.LEFT); mGestureRecognized = true; } else if (vYTotal > VELOCITY_THRESHOLD_Y2 && absVXTotal < VELOCITY_THRESHOLD_X2) { if (mReponseGestures) onDirectionGesture(Gravity.BOTTOM); mGestureRecognized = true; } else if (vYTotal < -VELOCITY_THRESHOLD_Y2 && absVXTotal < VELOCITY_THRESHOLD_X2) { if (mReponseGestures) onDirectionGesture(Gravity.TOP); mGestureRecognized = true; } mTimeLastOnScroll = timeNow; return mGestureRecognized; } @Override public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) { return mGestureRecognized; } public void onDirectionGesture(int gravity) { if (Gravity.NO_GRAVITY == gravity) { return; } if (Gravity.LEFT == gravity || Gravity.RIGHT == gravity) { if (mCandidatesContainer.isShown()) { if (Gravity.LEFT == gravity) { mCandidatesContainer.pageForward(true, true); } else { mCandidatesContainer.pageBackward(true, true); } return; } } } } /** * Connection used for binding to the Pinyin decoding service. */ public class PinyinDecoderServiceConnection implements ServiceConnection { public void onServiceConnected(ComponentName name, IBinder service) { mDecInfo.mIPinyinDecoderService = IPinyinDecoderService.Stub .asInterface(service); } public void onServiceDisconnected(ComponentName name) { } } public enum ImeState { STATE_BYPASS, STATE_IDLE, STATE_INPUT, STATE_COMPOSING, STATE_PREDICT, STATE_APP_COMPLETION } public class DecodingInfo { /** * Maximum length of the Pinyin string */ private static final int PY_STRING_MAX = 28; /** * Maximum number of candidates to display in one page. */ private static final int MAX_PAGE_SIZE_DISPLAY = 10; /** * Spelling (Pinyin) string. */ private StringBuffer mSurface; /** * Byte buffer used as the Pinyin string parameter for native function * call. */ private byte mPyBuf[]; /** * The length of surface string successfully decoded by engine. */ private int mSurfaceDecodedLen; /** * Composing string. */ private String mComposingStr; /** * Length of the active composing string. */ private int mActiveCmpsLen; /** * Composing string for display, it is copied from mComposingStr, and * add spaces between spellings. **/ private String mComposingStrDisplay; /** * Length of the active composing string for display. */ private int mActiveCmpsDisplayLen; /** * The first full sentence choice. */ private String mFullSent; /** * Number of characters which have been fixed. */ private int mFixedLen; /** * If this flag is true, selection is finished. */ private boolean mFinishSelection; /** * The starting position for each spelling. The first one is the number * of the real starting position elements. */ private int mSplStart[]; /** * Editing cursor in mSurface. */ private int mCursorPos; /** * Remote Pinyin-to-Hanzi decoding engine service. */ private IPinyinDecoderService mIPinyinDecoderService; /** * The complication information suggested by application. */ private CompletionInfo[] mAppCompletions; /** * The total number of choices for display. The list may only contains * the first part. If user tries to navigate to next page which is not * in the result list, we need to get these items. **/ public int mTotalChoicesNum; /** * Candidate list. The first one is the full-sentence candidate. */ public List mCandidatesList = new Vector(); /** * Element i stores the starting position of page i. */ public Vector mPageStart = new Vector(); /** * Element i stores the number of characters to page i. */ public Vector mCnToPage = new Vector(); /** * The position to delete in Pinyin string. If it is less than 0, IME * will do an incremental search, otherwise IME will do a deletion * operation. if {@link #mIsPosInSpl} is true, IME will delete the whole * string for mPosDelSpl-th spelling, otherwise it will only delete * mPosDelSpl-th character in the Pinyin string. */ public int mPosDelSpl = -1; /** * If {@link #mPosDelSpl} is big than or equal to 0, this member is used * to indicate that whether the postion is counted in spelling id or * character. */ public boolean mIsPosInSpl; public DecodingInfo() { mSurface = new StringBuffer(); mSurfaceDecodedLen = 0; } public void reset() { mSurface.delete(0, mSurface.length()); mSurfaceDecodedLen = 0; mCursorPos = 0; mFullSent = ""; mFixedLen = 0; mFinishSelection = false; mComposingStr = ""; mComposingStrDisplay = ""; mActiveCmpsLen = 0; mActiveCmpsDisplayLen = 0; resetCandidates(); } public boolean isCandidatesListEmpty() { return mCandidatesList.size() == 0; } public boolean isSplStrFull() { if (mSurface.length() >= PY_STRING_MAX - 1) return true; return false; } public void addSplChar(char ch, boolean reset) { if (reset) { mSurface.delete(0, mSurface.length()); mSurfaceDecodedLen = 0; mCursorPos = 0; try { mIPinyinDecoderService.imResetSearch(); } catch (RemoteException e) { } } mSurface.insert(mCursorPos, ch); mCursorPos++; } // Prepare to delete before cursor. We may delete a spelling char if // the cursor is in the range of unfixed part, delete a whole spelling // if the cursor in inside the range of the fixed part. // This function only marks the position used to delete. public void prepareDeleteBeforeCursor() { if (mCursorPos > 0) { int pos; for (pos = 0; pos < mFixedLen; pos++) { if (mSplStart[pos + 2] >= mCursorPos && mSplStart[pos + 1] < mCursorPos) { mPosDelSpl = pos; mCursorPos = mSplStart[pos + 1]; mIsPosInSpl = true; break; } } if (mPosDelSpl < 0) { mPosDelSpl = mCursorPos - 1; mCursorPos--; mIsPosInSpl = false; } } } public int length() { return mSurface.length(); } public char charAt(int index) { return mSurface.charAt(index); } public StringBuffer getOrigianlSplStr() { return mSurface; } public int getSplStrDecodedLen() { return mSurfaceDecodedLen; } public int[] getSplStart() { return mSplStart; } public String getComposingStr() { return mComposingStr; } public String getComposingStrActivePart() { assert (mActiveCmpsLen <= mComposingStr.length()); return mComposingStr.substring(0, mActiveCmpsLen); } public int getActiveCmpsLen() { return mActiveCmpsLen; } public String getComposingStrForDisplay() { return mComposingStrDisplay; } public int getActiveCmpsDisplayLen() { return mActiveCmpsDisplayLen; } public String getFullSent() { return mFullSent; } public String getCurrentFullSent(int activeCandPos) { try { String retStr = mFullSent.substring(0, mFixedLen); retStr += mCandidatesList.get(activeCandPos); return retStr; } catch (Exception e) { return ""; } } public void resetCandidates() { mCandidatesList.clear(); mTotalChoicesNum = 0; mPageStart.clear(); mPageStart.add(0); mCnToPage.clear(); mCnToPage.add(0); } public boolean candidatesFromApp() { return ImeState.STATE_APP_COMPLETION == mImeState; } public boolean canDoPrediction() { return mComposingStr.length() == mFixedLen; } public boolean selectionFinished() { return mFinishSelection; } // After the user chooses a candidate, input method will do a // re-decoding and give the new candidate list. // If candidate id is less than 0, means user is inputting Pinyin, // not selecting any choice. private void chooseDecodingCandidate(int candId) { if (mImeState != ImeState.STATE_PREDICT) { resetCandidates(); int totalChoicesNum = 0; try { if (candId < 0) { if (length() == 0) { totalChoicesNum = 0; } else { if (mPyBuf == null) mPyBuf = new byte[PY_STRING_MAX]; for (int i = 0; i < length(); i++) mPyBuf[i] = (byte) charAt(i); mPyBuf[length()] = 0; if (mPosDelSpl < 0) { totalChoicesNum = mIPinyinDecoderService .imSearch(mPyBuf, length()); } else { boolean clear_fixed_this_step = true; if (ImeState.STATE_COMPOSING == mImeState) { clear_fixed_this_step = false; } totalChoicesNum = mIPinyinDecoderService .imDelSearch(mPosDelSpl, mIsPosInSpl, clear_fixed_this_step); mPosDelSpl = -1; } } } else { totalChoicesNum = mIPinyinDecoderService .imChoose(candId); } } catch (RemoteException e) { } updateDecInfoForSearch(totalChoicesNum); } } private void updateDecInfoForSearch(int totalChoicesNum) { mTotalChoicesNum = totalChoicesNum; if (mTotalChoicesNum < 0) { mTotalChoicesNum = 0; return; } try { String pyStr; mSplStart = mIPinyinDecoderService.imGetSplStart(); pyStr = mIPinyinDecoderService.imGetPyStr(false); mSurfaceDecodedLen = mIPinyinDecoderService.imGetPyStrLen(true); assert (mSurfaceDecodedLen <= pyStr.length()); mFullSent = mIPinyinDecoderService.imGetChoice(0); mFixedLen = mIPinyinDecoderService.imGetFixedLen(); // Update the surface string to the one kept by engine. mSurface.replace(0, mSurface.length(), pyStr); if (mCursorPos > mSurface.length()) mCursorPos = mSurface.length(); mComposingStr = mFullSent.substring(0, mFixedLen) + mSurface.substring(mSplStart[mFixedLen + 1]); mActiveCmpsLen = mComposingStr.length(); if (mSurfaceDecodedLen > 0) { mActiveCmpsLen = mActiveCmpsLen - (mSurface.length() - mSurfaceDecodedLen); } // Prepare the display string. if (0 == mSurfaceDecodedLen) { mComposingStrDisplay = mComposingStr; mActiveCmpsDisplayLen = mComposingStr.length(); } else { mComposingStrDisplay = mFullSent.substring(0, mFixedLen); for (int pos = mFixedLen + 1; pos < mSplStart.length - 1; pos++) { mComposingStrDisplay += mSurface.substring( mSplStart[pos], mSplStart[pos + 1]); if (mSplStart[pos + 1] < mSurfaceDecodedLen) { mComposingStrDisplay += " "; } } mActiveCmpsDisplayLen = mComposingStrDisplay.length(); if (mSurfaceDecodedLen < mSurface.length()) { mComposingStrDisplay += mSurface .substring(mSurfaceDecodedLen); } } if (mSplStart.length == mFixedLen + 2) { mFinishSelection = true; } else { mFinishSelection = false; } } catch (RemoteException e) { Log.w(TAG, "PinyinDecoderService died", e); } catch (Exception e) { mTotalChoicesNum = 0; mComposingStr = ""; } // Prepare page 0. if (!mFinishSelection) { preparePage(0); } } private void choosePredictChoice(int choiceId) { if (ImeState.STATE_PREDICT != mImeState || choiceId < 0 || choiceId >= mTotalChoicesNum) { return; } String tmp = mCandidatesList.get(choiceId); resetCandidates(); mCandidatesList.add(tmp); mTotalChoicesNum = 1; mSurface.replace(0, mSurface.length(), ""); mCursorPos = 0; mFullSent = tmp; mFixedLen = tmp.length(); mComposingStr = mFullSent; mActiveCmpsLen = mFixedLen; mFinishSelection = true; } public String getCandidate(int candId) { // Only loaded items can be gotten, so we use mCandidatesList.size() // instead mTotalChoiceNum. if (candId < 0 || candId > mCandidatesList.size()) { return null; } return mCandidatesList.get(candId); } private void getCandiagtesForCache() { int fetchStart = mCandidatesList.size(); int fetchSize = mTotalChoicesNum - fetchStart; if (fetchSize > MAX_PAGE_SIZE_DISPLAY) { fetchSize = MAX_PAGE_SIZE_DISPLAY; } try { List newList = null; if (ImeState.STATE_INPUT == mImeState || ImeState.STATE_IDLE == mImeState || ImeState.STATE_COMPOSING == mImeState){ newList = mIPinyinDecoderService.imGetChoiceList( fetchStart, fetchSize, mFixedLen); } else if (ImeState.STATE_PREDICT == mImeState) { newList = mIPinyinDecoderService.imGetPredictList( fetchStart, fetchSize); } else if (ImeState.STATE_APP_COMPLETION == mImeState) { newList = new ArrayList(); if (null != mAppCompletions) { for (int pos = fetchStart; pos < fetchSize; pos++) { CompletionInfo ci = mAppCompletions[pos]; if (null != ci) { CharSequence s = ci.getText(); if (null != s) newList.add(s.toString()); } } } } mCandidatesList.addAll(newList); } catch (RemoteException e) { Log.w(TAG, "PinyinDecoderService died", e); } } public boolean pageReady(int pageNo) { // If the page number is less than 0, return false if (pageNo < 0) return false; // Page pageNo's ending information is not ready. if (mPageStart.size() <= pageNo + 1) { return false; } return true; } public boolean preparePage(int pageNo) { // If the page number is less than 0, return false if (pageNo < 0) return false; // Make sure the starting information for page pageNo is ready. if (mPageStart.size() <= pageNo) { return false; } // Page pageNo's ending information is also ready. if (mPageStart.size() > pageNo + 1) { return true; } // If cached items is enough for page pageNo. if (mCandidatesList.size() - mPageStart.elementAt(pageNo) >= MAX_PAGE_SIZE_DISPLAY) { return true; } // Try to get more items from engine getCandiagtesForCache(); // Try to find if there are available new items to display. // If no new item, return false; if (mPageStart.elementAt(pageNo) >= mCandidatesList.size()) { return false; } // If there are new items, return true; return true; } public void preparePredicts(CharSequence history) { if (null == history) return; resetCandidates(); if (Settings.getPrediction()) { String preEdit = history.toString(); int predictNum = 0; if (null != preEdit) { try { mTotalChoicesNum = mIPinyinDecoderService .imGetPredictsNum(preEdit); } catch (RemoteException e) { return; } } } preparePage(0); mFinishSelection = false; } private void prepareAppCompletions(CompletionInfo completions[]) { resetCandidates(); mAppCompletions = completions; mTotalChoicesNum = completions.length; preparePage(0); mFinishSelection = false; return; } public int getCurrentPageSize(int currentPage) { if (mPageStart.size() <= currentPage + 1) return 0; return mPageStart.elementAt(currentPage + 1) - mPageStart.elementAt(currentPage); } public int getCurrentPageStart(int currentPage) { if (mPageStart.size() < currentPage + 1) return mTotalChoicesNum; return mPageStart.elementAt(currentPage); } public boolean pageForwardable(int currentPage) { if (mPageStart.size() <= currentPage + 1) return false; if (mPageStart.elementAt(currentPage + 1) >= mTotalChoicesNum) { return false; } return true; } public boolean pageBackwardable(int currentPage) { if (currentPage > 0) return true; return false; } public boolean charBeforeCursorIsSeparator() { int len = mSurface.length(); if (mCursorPos > len) return false; if (mCursorPos > 0 && mSurface.charAt(mCursorPos - 1) == '\'') { return true; } return false; } public int getCursorPos() { return mCursorPos; } public int getCursorPosInCmps() { int cursorPos = mCursorPos; int fixedLen = 0; for (int hzPos = 0; hzPos < mFixedLen; hzPos++) { if (mCursorPos >= mSplStart[hzPos + 2]) { cursorPos -= mSplStart[hzPos + 2] - mSplStart[hzPos + 1]; cursorPos += 1; } } return cursorPos; } public int getCursorPosInCmpsDisplay() { int cursorPos = getCursorPosInCmps(); // +2 is because: one for mSplStart[0], which is used for other // purpose(The length of the segmentation string), and another // for the first spelling which does not need a space before it. for (int pos = mFixedLen + 2; pos < mSplStart.length - 1; pos++) { if (mCursorPos <= mSplStart[pos]) { break; } else { cursorPos++; } } return cursorPos; } public void moveCursorToEdge(boolean left) { if (left) mCursorPos = 0; else mCursorPos = mSurface.length(); } // Move cursor. If offset is 0, this function can be used to adjust // the cursor into the bounds of the string. public void moveCursor(int offset) { if (offset > 1 || offset < -1) return; if (offset != 0) { int hzPos = 0; for (hzPos = 0; hzPos <= mFixedLen; hzPos++) { if (mCursorPos == mSplStart[hzPos + 1]) { if (offset < 0) { if (hzPos > 0) { offset = mSplStart[hzPos] - mSplStart[hzPos + 1]; } } else { if (hzPos < mFixedLen) { offset = mSplStart[hzPos + 2] - mSplStart[hzPos + 1]; } } break; } } } mCursorPos += offset; if (mCursorPos < 0) { mCursorPos = 0; } else if (mCursorPos > mSurface.length()) { mCursorPos = mSurface.length(); } } public int getSplNum() { return mSplStart[0]; } public int getFixedLen() { return mFixedLen; } } }