diff options
Diffstat (limited to 'src/com/android/inputmethod/pinyin/PinyinIME.java')
-rw-r--r-- | src/com/android/inputmethod/pinyin/PinyinIME.java | 2134 |
1 files changed, 2134 insertions, 0 deletions
diff --git a/src/com/android/inputmethod/pinyin/PinyinIME.java b/src/com/android/inputmethod/pinyin/PinyinIME.java new file mode 100644 index 0000000..9ac2c2d --- /dev/null +++ b/src/com/android/inputmethod/pinyin/PinyinIME.java @@ -0,0 +1,2134 @@ +/* + * 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(PinyinIME.this) + .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<String> mCandidatesList = new Vector<String>(); + + /** + * Element i stores the starting position of page i. + */ + public Vector<Integer> mPageStart = new Vector<Integer>(); + + /** + * Element i stores the number of characters to page i. + */ + public Vector<Integer> mCnToPage = new Vector<Integer>(); + + /** + * 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<String> 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<String>(); + 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; + } + } +} |