1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.pinyin;
18
19import android.app.AlertDialog;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.content.Intent;
25import android.content.ServiceConnection;
26import android.content.res.Configuration;
27import android.inputmethodservice.InputMethodService;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.preference.PreferenceManager;
32import android.util.Log;
33import android.view.Gravity;
34import android.view.GestureDetector;
35import android.view.LayoutInflater;
36import android.view.KeyEvent;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.Window;
40import android.view.WindowManager;
41import android.view.View.MeasureSpec;
42import android.view.ViewGroup.LayoutParams;
43import android.view.inputmethod.CompletionInfo;
44import android.view.inputmethod.InputConnection;
45import android.view.inputmethod.EditorInfo;
46import android.view.inputmethod.InputMethodManager;
47import android.widget.LinearLayout;
48import android.widget.PopupWindow;
49
50import java.util.ArrayList;
51import java.util.List;
52import java.util.Vector;
53
54/**
55 * Main class of the Pinyin input method.
56 */
57public class PinyinIME extends InputMethodService {
58    /**
59     * TAG for debug.
60     */
61    static final String TAG = "PinyinIME";
62
63    /**
64     * If is is true, IME will simulate key events for delete key, and send the
65     * events back to the application.
66     */
67    private static final boolean SIMULATE_KEY_DELETE = true;
68
69    /**
70     * Necessary environment configurations like screen size for this IME.
71     */
72    private Environment mEnvironment;
73
74    /**
75     * Used to switch input mode.
76     */
77    private InputModeSwitcher mInputModeSwitcher;
78
79    /**
80     * Soft keyboard container view to host real soft keyboard view.
81     */
82    private SkbContainer mSkbContainer;
83
84    /**
85     * The floating container which contains the composing view. If necessary,
86     * some other view like candiates container can also be put here.
87     */
88    private LinearLayout mFloatingContainer;
89
90    /**
91     * View to show the composing string.
92     */
93    private ComposingView mComposingView;
94
95    /**
96     * Window to show the composing string.
97     */
98    private PopupWindow mFloatingWindow;
99
100    /**
101     * Used to show the floating window.
102     */
103    private PopupTimer mFloatingWindowTimer = new PopupTimer();
104
105    /**
106     * View to show candidates list.
107     */
108    private CandidatesContainer mCandidatesContainer;
109
110    /**
111     * Balloon used when user presses a candidate.
112     */
113    private BalloonHint mCandidatesBalloon;
114
115    /**
116     * Used to notify the input method when the user touch a candidate.
117     */
118    private ChoiceNotifier mChoiceNotifier;
119
120    /**
121     * Used to notify gestures from soft keyboard.
122     */
123    private OnGestureListener mGestureListenerSkb;
124
125    /**
126     * Used to notify gestures from candidates view.
127     */
128    private OnGestureListener mGestureListenerCandidates;
129
130    /**
131     * The on-screen movement gesture detector for soft keyboard.
132     */
133    private GestureDetector mGestureDetectorSkb;
134
135    /**
136     * The on-screen movement gesture detector for candidates view.
137     */
138    private GestureDetector mGestureDetectorCandidates;
139
140    /**
141     * Option dialog to choose settings and other IMEs.
142     */
143    private AlertDialog mOptionsDialog;
144
145    /**
146     * Connection used to bind the decoding service.
147     */
148    private PinyinDecoderServiceConnection mPinyinDecoderServiceConnection;
149
150    /**
151     * The current IME status.
152     *
153     * @see com.android.inputmethod.pinyin.PinyinIME.ImeState
154     */
155    private ImeState mImeState = ImeState.STATE_IDLE;
156
157    /**
158     * The decoding information, include spelling(Pinyin) string, decoding
159     * result, etc.
160     */
161    private DecodingInfo mDecInfo = new DecodingInfo();
162
163    /**
164     * For English input.
165     */
166    private EnglishInputProcessor mImEn;
167
168    // receive ringer mode changes
169    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
170        @Override
171        public void onReceive(Context context, Intent intent) {
172            SoundManager.getInstance(context).updateRingerMode();
173        }
174    };
175
176    @Override
177    public void onCreate() {
178        mEnvironment = Environment.getInstance();
179        if (mEnvironment.needDebug()) {
180            Log.d(TAG, "onCreate.");
181        }
182        super.onCreate();
183
184        startPinyinDecoderService();
185        mImEn = new EnglishInputProcessor();
186        Settings.getInstance(PreferenceManager
187                .getDefaultSharedPreferences(getApplicationContext()));
188
189        mInputModeSwitcher = new InputModeSwitcher(this);
190        mChoiceNotifier = new ChoiceNotifier(this);
191        mGestureListenerSkb = new OnGestureListener(false);
192        mGestureListenerCandidates = new OnGestureListener(true);
193        mGestureDetectorSkb = new GestureDetector(this, mGestureListenerSkb);
194        mGestureDetectorCandidates = new GestureDetector(this,
195                mGestureListenerCandidates);
196
197        mEnvironment.onConfigurationChanged(getResources().getConfiguration(),
198                this);
199    }
200
201    @Override
202    public void onDestroy() {
203        if (mEnvironment.needDebug()) {
204            Log.d(TAG, "onDestroy.");
205        }
206        unbindService(mPinyinDecoderServiceConnection);
207        Settings.releaseInstance();
208        super.onDestroy();
209    }
210
211    @Override
212    public void onConfigurationChanged(Configuration newConfig) {
213        Environment env = Environment.getInstance();
214        if (mEnvironment.needDebug()) {
215            Log.d(TAG, "onConfigurationChanged");
216            Log.d(TAG, "--last config: " + env.getConfiguration().toString());
217            Log.d(TAG, "---new config: " + newConfig.toString());
218        }
219        // We need to change the local environment first so that UI components
220        // can get the environment instance to handle size issues. When
221        // super.onConfigurationChanged() is called, onCreateCandidatesView()
222        // and onCreateInputView() will be executed if necessary.
223        env.onConfigurationChanged(newConfig, this);
224
225        // Clear related UI of the previous configuration.
226        if (null != mSkbContainer) {
227            mSkbContainer.dismissPopups();
228        }
229        if (null != mCandidatesBalloon) {
230            mCandidatesBalloon.dismiss();
231        }
232        super.onConfigurationChanged(newConfig);
233        resetToIdleState(false);
234    }
235
236    @Override
237    public boolean onKeyDown(int keyCode, KeyEvent event) {
238        if (processKey(event, 0 != event.getRepeatCount())) return true;
239        return super.onKeyDown(keyCode, event);
240    }
241
242    @Override
243    public boolean onKeyUp(int keyCode, KeyEvent event) {
244        if (processKey(event, true)) return true;
245        return super.onKeyUp(keyCode, event);
246    }
247
248    private boolean processKey(KeyEvent event, boolean realAction) {
249        if (ImeState.STATE_BYPASS == mImeState) return false;
250
251        int keyCode = event.getKeyCode();
252        // SHIFT-SPACE is used to switch between Chinese and English
253        // when HKB is on.
254        if (KeyEvent.KEYCODE_SPACE == keyCode && event.isShiftPressed()) {
255            if (!realAction) return true;
256
257            updateIcon(mInputModeSwitcher.switchLanguageWithHkb());
258            resetToIdleState(false);
259
260            int allMetaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON
261                    | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_SHIFT_ON
262                    | KeyEvent.META_SHIFT_LEFT_ON
263                    | KeyEvent.META_SHIFT_RIGHT_ON | KeyEvent.META_SYM_ON;
264            getCurrentInputConnection().clearMetaKeyStates(allMetaState);
265            return true;
266        }
267
268        // If HKB is on to input English, by-pass the key event so that
269        // default key listener will handle it.
270        if (mInputModeSwitcher.isEnglishWithHkb()) {
271            return false;
272        }
273
274        if (processFunctionKeys(keyCode, realAction)) {
275            return true;
276        }
277
278        int keyChar = 0;
279        if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
280            keyChar = keyCode - KeyEvent.KEYCODE_A + 'a';
281        } else if (keyCode >= KeyEvent.KEYCODE_0
282                && keyCode <= KeyEvent.KEYCODE_9) {
283            keyChar = keyCode - KeyEvent.KEYCODE_0 + '0';
284        } else if (keyCode == KeyEvent.KEYCODE_COMMA) {
285            keyChar = ',';
286        } else if (keyCode == KeyEvent.KEYCODE_PERIOD) {
287            keyChar = '.';
288        } else if (keyCode == KeyEvent.KEYCODE_SPACE) {
289            keyChar = ' ';
290        } else if (keyCode == KeyEvent.KEYCODE_APOSTROPHE) {
291            keyChar = '\'';
292        }
293
294        if (mInputModeSwitcher.isEnglishWithSkb()) {
295            return mImEn.processKey(getCurrentInputConnection(), event,
296                    mInputModeSwitcher.isEnglishUpperCaseWithSkb(), realAction);
297        } else if (mInputModeSwitcher.isChineseText()) {
298            if (mImeState == ImeState.STATE_IDLE ||
299                    mImeState == ImeState.STATE_APP_COMPLETION) {
300                mImeState = ImeState.STATE_IDLE;
301                return processStateIdle(keyChar, keyCode, event, realAction);
302            } else if (mImeState == ImeState.STATE_INPUT) {
303                return processStateInput(keyChar, keyCode, event, realAction);
304            } else if (mImeState == ImeState.STATE_PREDICT) {
305                return processStatePredict(keyChar, keyCode, event, realAction);
306            } else if (mImeState == ImeState.STATE_COMPOSING) {
307                return processStateEditComposing(keyChar, keyCode, event,
308                        realAction);
309            }
310        } else {
311            if (0 != keyChar && realAction) {
312                commitResultText(String.valueOf((char) keyChar));
313            }
314        }
315
316        return false;
317    }
318
319    // keyCode can be from both hard key or soft key.
320    private boolean processFunctionKeys(int keyCode, boolean realAction) {
321        // Back key is used to dismiss all popup UI in a soft keyboard.
322        if (keyCode == KeyEvent.KEYCODE_BACK) {
323            if (isInputViewShown()) {
324                if (mSkbContainer.handleBack(realAction)) return true;
325            }
326        }
327
328        // Chinese related input is handle separately.
329        if (mInputModeSwitcher.isChineseText()) {
330            return false;
331        }
332
333        if (null != mCandidatesContainer && mCandidatesContainer.isShown()
334                && !mDecInfo.isCandidatesListEmpty()) {
335            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
336                if (!realAction) return true;
337
338                chooseCandidate(-1);
339                return true;
340            }
341
342            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
343                if (!realAction) return true;
344                mCandidatesContainer.activeCurseBackward();
345                return true;
346            }
347
348            if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
349                if (!realAction) return true;
350                mCandidatesContainer.activeCurseForward();
351                return true;
352            }
353
354            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
355                if (!realAction) return true;
356                mCandidatesContainer.pageBackward(false, true);
357                return true;
358            }
359
360            if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
361                if (!realAction) return true;
362                mCandidatesContainer.pageForward(false, true);
363                return true;
364            }
365
366            if (keyCode == KeyEvent.KEYCODE_DEL &&
367                    ImeState.STATE_PREDICT == mImeState) {
368                if (!realAction) return true;
369                resetToIdleState(false);
370                return true;
371            }
372        } else {
373            if (keyCode == KeyEvent.KEYCODE_DEL) {
374                if (!realAction) return true;
375                if (SIMULATE_KEY_DELETE) {
376                    simulateKeyEventDownUp(keyCode);
377                } else {
378                    getCurrentInputConnection().deleteSurroundingText(1, 0);
379                }
380                return true;
381            }
382            if (keyCode == KeyEvent.KEYCODE_ENTER) {
383                if (!realAction) return true;
384                sendKeyChar('\n');
385                return true;
386            }
387            if (keyCode == KeyEvent.KEYCODE_SPACE) {
388                if (!realAction) return true;
389                sendKeyChar(' ');
390                return true;
391            }
392        }
393
394        return false;
395    }
396
397    private boolean processStateIdle(int keyChar, int keyCode, KeyEvent event,
398            boolean realAction) {
399        // In this status, when user presses keys in [a..z], the status will
400        // change to input state.
401        if (keyChar >= 'a' && keyChar <= 'z' && !event.isAltPressed()) {
402            if (!realAction) return true;
403            mDecInfo.addSplChar((char) keyChar, true);
404            chooseAndUpdate(-1);
405            return true;
406        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
407            if (!realAction) return true;
408            if (SIMULATE_KEY_DELETE) {
409                simulateKeyEventDownUp(keyCode);
410            } else {
411                getCurrentInputConnection().deleteSurroundingText(1, 0);
412            }
413            return true;
414        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
415            if (!realAction) return true;
416            sendKeyChar('\n');
417            return true;
418        } else if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
419                || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
420                || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
421                || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
422            return true;
423        } else if (event.isAltPressed()) {
424            char fullwidth_char = KeyMapDream.getChineseLabel(keyCode);
425            if (0 != fullwidth_char) {
426                if (realAction) {
427                    String result = String.valueOf(fullwidth_char);
428                    commitResultText(result);
429                }
430                return true;
431            } else {
432                if (keyCode >= KeyEvent.KEYCODE_A
433                        && keyCode <= KeyEvent.KEYCODE_Z) {
434                    return true;
435                }
436            }
437        } else if (keyChar != 0 && keyChar != '\t') {
438            if (realAction) {
439                if (keyChar == ',' || keyChar == '.') {
440                    inputCommaPeriod("", keyChar, false, ImeState.STATE_IDLE);
441                } else {
442                    if (0 != keyChar) {
443                        String result = String.valueOf((char) keyChar);
444                        commitResultText(result);
445                    }
446                }
447            }
448            return true;
449        }
450        return false;
451    }
452
453    private boolean processStateInput(int keyChar, int keyCode, KeyEvent event,
454            boolean realAction) {
455        // If ALT key is pressed, input alternative key. But if the
456        // alternative key is quote key, it will be used for input a splitter
457        // in Pinyin string.
458        if (event.isAltPressed()) {
459            if ('\'' != event.getUnicodeChar(event.getMetaState())) {
460                if (realAction) {
461                    char fullwidth_char = KeyMapDream.getChineseLabel(keyCode);
462                    if (0 != fullwidth_char) {
463                        commitResultText(mDecInfo
464                                .getCurrentFullSent(mCandidatesContainer
465                                        .getActiveCandiatePos()) +
466                                        String.valueOf(fullwidth_char));
467                        resetToIdleState(false);
468                    }
469                }
470                return true;
471            } else {
472                keyChar = '\'';
473            }
474        }
475
476        if (keyChar >= 'a' && keyChar <= 'z' || keyChar == '\''
477                && !mDecInfo.charBeforeCursorIsSeparator()
478                || keyCode == KeyEvent.KEYCODE_DEL) {
479            if (!realAction) return true;
480            return processSurfaceChange(keyChar, keyCode);
481        } else if (keyChar == ',' || keyChar == '.') {
482            if (!realAction) return true;
483            inputCommaPeriod(mDecInfo.getCurrentFullSent(mCandidatesContainer
484                    .getActiveCandiatePos()), keyChar, true,
485                    ImeState.STATE_IDLE);
486            return true;
487        } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP
488                || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
489                || keyCode == KeyEvent.KEYCODE_DPAD_LEFT
490                || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
491            if (!realAction) return true;
492
493            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
494                mCandidatesContainer.activeCurseBackward();
495            } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
496                mCandidatesContainer.activeCurseForward();
497            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
498                // If it has been the first page, a up key will shift
499                // the state to edit composing string.
500                if (!mCandidatesContainer.pageBackward(false, true)) {
501                    mCandidatesContainer.enableActiveHighlight(false);
502                    changeToStateComposing(true);
503                    updateComposingText(true);
504                }
505            } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
506                mCandidatesContainer.pageForward(false, true);
507            }
508            return true;
509        } else if (keyCode >= KeyEvent.KEYCODE_1
510                && keyCode <= KeyEvent.KEYCODE_9) {
511            if (!realAction) return true;
512
513            int activePos = keyCode - KeyEvent.KEYCODE_1;
514            int currentPage = mCandidatesContainer.getCurrentPage();
515            if (activePos < mDecInfo.getCurrentPageSize(currentPage)) {
516                activePos = activePos
517                        + mDecInfo.getCurrentPageStart(currentPage);
518                if (activePos >= 0) {
519                    chooseAndUpdate(activePos);
520                }
521            }
522            return true;
523        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
524            if (!realAction) return true;
525            if (mInputModeSwitcher.isEnterNoramlState()) {
526                commitResultText(mDecInfo.getOrigianlSplStr().toString());
527                resetToIdleState(false);
528            } else {
529                commitResultText(mDecInfo
530                        .getCurrentFullSent(mCandidatesContainer
531                                .getActiveCandiatePos()));
532                sendKeyChar('\n');
533                resetToIdleState(false);
534            }
535            return true;
536        } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
537                || keyCode == KeyEvent.KEYCODE_SPACE) {
538            if (!realAction) return true;
539            chooseCandidate(-1);
540            return true;
541        } else if (keyCode == KeyEvent.KEYCODE_BACK) {
542            if (!realAction) return true;
543            resetToIdleState(false);
544            requestHideSelf(0);
545            return true;
546        }
547        return false;
548    }
549
550    private boolean processStatePredict(int keyChar, int keyCode,
551            KeyEvent event, boolean realAction) {
552        if (!realAction) return true;
553
554        // If ALT key is pressed, input alternative key.
555        if (event.isAltPressed()) {
556            char fullwidth_char = KeyMapDream.getChineseLabel(keyCode);
557            if (0 != fullwidth_char) {
558                commitResultText(mDecInfo.getCandidate(mCandidatesContainer
559                                .getActiveCandiatePos()) +
560                                String.valueOf(fullwidth_char));
561                resetToIdleState(false);
562            }
563            return true;
564        }
565
566        // In this status, when user presses keys in [a..z], the status will
567        // change to input state.
568        if (keyChar >= 'a' && keyChar <= 'z') {
569            changeToStateInput(true);
570            mDecInfo.addSplChar((char) keyChar, true);
571            chooseAndUpdate(-1);
572        } else if (keyChar == ',' || keyChar == '.') {
573            inputCommaPeriod("", keyChar, true, ImeState.STATE_IDLE);
574        } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP
575                || keyCode == KeyEvent.KEYCODE_DPAD_DOWN
576                || keyCode == KeyEvent.KEYCODE_DPAD_LEFT
577                || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
578            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
579                mCandidatesContainer.activeCurseBackward();
580            }
581            if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
582                mCandidatesContainer.activeCurseForward();
583            }
584            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
585                mCandidatesContainer.pageBackward(false, true);
586            }
587            if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
588                mCandidatesContainer.pageForward(false, true);
589            }
590        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
591            resetToIdleState(false);
592        } else if (keyCode == KeyEvent.KEYCODE_BACK) {
593            resetToIdleState(false);
594            requestHideSelf(0);
595        } else if (keyCode >= KeyEvent.KEYCODE_1
596                && keyCode <= KeyEvent.KEYCODE_9) {
597            int activePos = keyCode - KeyEvent.KEYCODE_1;
598            int currentPage = mCandidatesContainer.getCurrentPage();
599            if (activePos < mDecInfo.getCurrentPageSize(currentPage)) {
600                activePos = activePos
601                        + mDecInfo.getCurrentPageStart(currentPage);
602                if (activePos >= 0) {
603                    chooseAndUpdate(activePos);
604                }
605            }
606        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
607            sendKeyChar('\n');
608            resetToIdleState(false);
609        } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
610                || keyCode == KeyEvent.KEYCODE_SPACE) {
611            chooseCandidate(-1);
612        }
613
614        return true;
615    }
616
617    private boolean processStateEditComposing(int keyChar, int keyCode,
618            KeyEvent event, boolean realAction) {
619        if (!realAction) return true;
620
621        ComposingView.ComposingStatus cmpsvStatus =
622                mComposingView.getComposingStatus();
623
624        // If ALT key is pressed, input alternative key. But if the
625        // alternative key is quote key, it will be used for input a splitter
626        // in Pinyin string.
627        if (event.isAltPressed()) {
628            if ('\'' != event.getUnicodeChar(event.getMetaState())) {
629                char fullwidth_char = KeyMapDream.getChineseLabel(keyCode);
630                if (0 != fullwidth_char) {
631                    String retStr;
632                    if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE ==
633                            cmpsvStatus) {
634                        retStr = mDecInfo.getOrigianlSplStr().toString();
635                    } else {
636                        retStr = mDecInfo.getComposingStr();
637                    }
638                    commitResultText(retStr + String.valueOf(fullwidth_char));
639                    resetToIdleState(false);
640                }
641                return true;
642            } else {
643                keyChar = '\'';
644            }
645        }
646
647        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
648            if (!mDecInfo.selectionFinished()) {
649                changeToStateInput(true);
650            }
651        } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
652                || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
653            mComposingView.moveCursor(keyCode);
654        } else if ((keyCode == KeyEvent.KEYCODE_ENTER && mInputModeSwitcher
655                .isEnterNoramlState())
656                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER
657                || keyCode == KeyEvent.KEYCODE_SPACE) {
658            if (ComposingView.ComposingStatus.SHOW_STRING_LOWERCASE == cmpsvStatus) {
659                String str = mDecInfo.getOrigianlSplStr().toString();
660                if (!tryInputRawUnicode(str)) {
661                    commitResultText(str);
662                }
663            } else if (ComposingView.ComposingStatus.EDIT_PINYIN == cmpsvStatus) {
664                String str = mDecInfo.getComposingStr();
665                if (!tryInputRawUnicode(str)) {
666                    commitResultText(str);
667                }
668            } else {
669                commitResultText(mDecInfo.getComposingStr());
670            }
671            resetToIdleState(false);
672        } else if (keyCode == KeyEvent.KEYCODE_ENTER
673                && !mInputModeSwitcher.isEnterNoramlState()) {
674            String retStr;
675            if (!mDecInfo.isCandidatesListEmpty()) {
676                retStr = mDecInfo.getCurrentFullSent(mCandidatesContainer
677                        .getActiveCandiatePos());
678            } else {
679                retStr = mDecInfo.getComposingStr();
680            }
681            commitResultText(retStr);
682            sendKeyChar('\n');
683            resetToIdleState(false);
684        } else if (keyCode == KeyEvent.KEYCODE_BACK) {
685            resetToIdleState(false);
686            requestHideSelf(0);
687            return true;
688        } else {
689            return processSurfaceChange(keyChar, keyCode);
690        }
691        return true;
692    }
693
694    private boolean tryInputRawUnicode(String str) {
695        if (str.length() > 7) {
696            if (str.substring(0, 7).compareTo("unicode") == 0) {
697                try {
698                    String digitStr = str.substring(7);
699                    int startPos = 0;
700                    int radix = 10;
701                    if (digitStr.length() > 2 && digitStr.charAt(0) == '0'
702                            && digitStr.charAt(1) == 'x') {
703                        startPos = 2;
704                        radix = 16;
705                    }
706                    digitStr = digitStr.substring(startPos);
707                    int unicode = Integer.parseInt(digitStr, radix);
708                    if (unicode > 0) {
709                        char low = (char) (unicode & 0x0000ffff);
710                        char high = (char) ((unicode & 0xffff0000) >> 16);
711                        commitResultText(String.valueOf(low));
712                        if (0 != high) {
713                            commitResultText(String.valueOf(high));
714                        }
715                    }
716                    return true;
717                } catch (NumberFormatException e) {
718                    return false;
719                }
720            } else if (str.substring(str.length() - 7, str.length()).compareTo(
721                    "unicode") == 0) {
722                String resultStr = "";
723                for (int pos = 0; pos < str.length() - 7; pos++) {
724                    if (pos > 0) {
725                        resultStr += " ";
726                    }
727
728                    resultStr += "0x" + Integer.toHexString(str.charAt(pos));
729                }
730                commitResultText(String.valueOf(resultStr));
731                return true;
732            }
733        }
734        return false;
735    }
736
737    private boolean processSurfaceChange(int keyChar, int keyCode) {
738        if (mDecInfo.isSplStrFull() && KeyEvent.KEYCODE_DEL != keyCode) {
739            return true;
740        }
741
742        if ((keyChar >= 'a' && keyChar <= 'z')
743                || (keyChar == '\'' && !mDecInfo.charBeforeCursorIsSeparator())
744                || (((keyChar >= '0' && keyChar <= '9') || keyChar == ' ') && ImeState.STATE_COMPOSING == mImeState)) {
745            mDecInfo.addSplChar((char) keyChar, false);
746            chooseAndUpdate(-1);
747        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
748            mDecInfo.prepareDeleteBeforeCursor();
749            chooseAndUpdate(-1);
750        }
751        return true;
752    }
753
754    private void changeToStateComposing(boolean updateUi) {
755        mImeState = ImeState.STATE_COMPOSING;
756        if (!updateUi) return;
757
758        if (null != mSkbContainer && mSkbContainer.isShown()) {
759            mSkbContainer.toggleCandidateMode(true);
760        }
761    }
762
763    private void changeToStateInput(boolean updateUi) {
764        mImeState = ImeState.STATE_INPUT;
765        if (!updateUi) return;
766
767        if (null != mSkbContainer && mSkbContainer.isShown()) {
768            mSkbContainer.toggleCandidateMode(true);
769        }
770        showCandidateWindow(true);
771    }
772
773    private void simulateKeyEventDownUp(int keyCode) {
774        InputConnection ic = getCurrentInputConnection();
775        if (null == ic) return;
776
777        ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
778        ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
779    }
780
781    private void commitResultText(String resultText) {
782        InputConnection ic = getCurrentInputConnection();
783        if (null != ic) ic.commitText(resultText, 1);
784        if (null != mComposingView) {
785            mComposingView.setVisibility(View.INVISIBLE);
786            mComposingView.invalidate();
787        }
788    }
789
790    private void updateComposingText(boolean visible) {
791        if (!visible) {
792            mComposingView.setVisibility(View.INVISIBLE);
793        } else {
794            mComposingView.setDecodingInfo(mDecInfo, mImeState);
795            mComposingView.setVisibility(View.VISIBLE);
796        }
797        mComposingView.invalidate();
798    }
799
800    private void inputCommaPeriod(String preEdit, int keyChar,
801            boolean dismissCandWindow, ImeState nextState) {
802        if (keyChar == ',')
803            preEdit += '\uff0c';
804        else if (keyChar == '.')
805            preEdit += '\u3002';
806        else
807            return;
808        commitResultText(preEdit);
809        if (dismissCandWindow) resetCandidateWindow();
810        mImeState = nextState;
811    }
812
813    private void resetToIdleState(boolean resetInlineText) {
814        if (ImeState.STATE_IDLE == mImeState) return;
815
816        mImeState = ImeState.STATE_IDLE;
817        mDecInfo.reset();
818
819        if (null != mComposingView) mComposingView.reset();
820        if (resetInlineText) commitResultText("");
821        resetCandidateWindow();
822    }
823
824    private void chooseAndUpdate(int candId) {
825        if (!mInputModeSwitcher.isChineseText()) {
826            String choice = mDecInfo.getCandidate(candId);
827            if (null != choice) {
828                commitResultText(choice);
829            }
830            resetToIdleState(false);
831            return;
832        }
833
834        if (ImeState.STATE_PREDICT != mImeState) {
835            // Get result candidate list, if choice_id < 0, do a new decoding.
836            // If choice_id >=0, select the candidate, and get the new candidate
837            // list.
838            mDecInfo.chooseDecodingCandidate(candId);
839        } else {
840            // Choose a prediction item.
841            mDecInfo.choosePredictChoice(candId);
842        }
843
844        if (mDecInfo.getComposingStr().length() > 0) {
845            String resultStr;
846            resultStr = mDecInfo.getComposingStrActivePart();
847
848            // choiceId >= 0 means user finishes a choice selection.
849            if (candId >= 0 && mDecInfo.canDoPrediction()) {
850                commitResultText(resultStr);
851                mImeState = ImeState.STATE_PREDICT;
852                if (null != mSkbContainer && mSkbContainer.isShown()) {
853                    mSkbContainer.toggleCandidateMode(false);
854                }
855                // Try to get the prediction list.
856                if (Settings.getPrediction()) {
857                    InputConnection ic = getCurrentInputConnection();
858                    if (null != ic) {
859                        CharSequence cs = ic.getTextBeforeCursor(3, 0);
860                        if (null != cs) {
861                            mDecInfo.preparePredicts(cs);
862                        }
863                    }
864                } else {
865                    mDecInfo.resetCandidates();
866                }
867
868                if (mDecInfo.mCandidatesList.size() > 0) {
869                    showCandidateWindow(false);
870                } else {
871                    resetToIdleState(false);
872                }
873            } else {
874                if (ImeState.STATE_IDLE == mImeState) {
875                    if (mDecInfo.getSplStrDecodedLen() == 0) {
876                        changeToStateComposing(true);
877                    } else {
878                        changeToStateInput(true);
879                    }
880                } else {
881                    if (mDecInfo.selectionFinished()) {
882                        changeToStateComposing(true);
883                    }
884                }
885                showCandidateWindow(true);
886            }
887        } else {
888            resetToIdleState(false);
889        }
890    }
891
892    // If activeCandNo is less than 0, get the current active candidate number
893    // from candidate view, otherwise use activeCandNo.
894    private void chooseCandidate(int activeCandNo) {
895        if (activeCandNo < 0) {
896            activeCandNo = mCandidatesContainer.getActiveCandiatePos();
897        }
898        if (activeCandNo >= 0) {
899            chooseAndUpdate(activeCandNo);
900        }
901    }
902
903    private boolean startPinyinDecoderService() {
904        if (null == mDecInfo.mIPinyinDecoderService) {
905            Intent serviceIntent = new Intent();
906            serviceIntent.setClass(this, PinyinDecoderService.class);
907
908            if (null == mPinyinDecoderServiceConnection) {
909                mPinyinDecoderServiceConnection = new PinyinDecoderServiceConnection();
910            }
911
912            // Bind service
913            if (bindService(serviceIntent, mPinyinDecoderServiceConnection,
914                    Context.BIND_AUTO_CREATE)) {
915                return true;
916            } else {
917                return false;
918            }
919        }
920        return true;
921    }
922
923    @Override
924    public View onCreateCandidatesView() {
925        if (mEnvironment.needDebug()) {
926            Log.d(TAG, "onCreateCandidatesView.");
927        }
928
929        LayoutInflater inflater = getLayoutInflater();
930        // Inflate the floating container view
931        mFloatingContainer = (LinearLayout) inflater.inflate(
932                R.layout.floating_container, null);
933
934        // The first child is the composing view.
935        mComposingView = (ComposingView) mFloatingContainer.getChildAt(0);
936
937        mCandidatesContainer = (CandidatesContainer) inflater.inflate(
938                R.layout.candidates_container, null);
939
940        // Create balloon hint for candidates view.
941        mCandidatesBalloon = new BalloonHint(this, mCandidatesContainer,
942                MeasureSpec.UNSPECIFIED);
943        mCandidatesBalloon.setBalloonBackground(getResources().getDrawable(
944                R.drawable.candidate_balloon_bg));
945        mCandidatesContainer.initialize(mChoiceNotifier, mCandidatesBalloon,
946                mGestureDetectorCandidates);
947
948        // The floating window
949        if (null != mFloatingWindow && mFloatingWindow.isShowing()) {
950            mFloatingWindowTimer.cancelShowing();
951            mFloatingWindow.dismiss();
952        }
953        mFloatingWindow = new PopupWindow(this);
954        mFloatingWindow.setClippingEnabled(false);
955        mFloatingWindow.setBackgroundDrawable(null);
956        mFloatingWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
957        mFloatingWindow.setContentView(mFloatingContainer);
958
959        setCandidatesViewShown(true);
960        return mCandidatesContainer;
961    }
962
963    public void responseSoftKeyEvent(SoftKey sKey) {
964        if (null == sKey) return;
965
966        InputConnection ic = getCurrentInputConnection();
967        if (ic == null) return;
968
969        int keyCode = sKey.getKeyCode();
970        // Process some general keys, including KEYCODE_DEL, KEYCODE_SPACE,
971        // KEYCODE_ENTER and KEYCODE_DPAD_CENTER.
972        if (sKey.isKeyCodeKey()) {
973            if (processFunctionKeys(keyCode, true)) return;
974        }
975
976        if (sKey.isUserDefKey()) {
977            updateIcon(mInputModeSwitcher.switchModeForUserKey(keyCode));
978            resetToIdleState(false);
979            mSkbContainer.updateInputMode();
980        } else {
981            if (sKey.isKeyCodeKey()) {
982                KeyEvent eDown = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN,
983                        keyCode, 0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD);
984                KeyEvent eUp = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode,
985                        0, 0, 0, 0, KeyEvent.FLAG_SOFT_KEYBOARD);
986
987                onKeyDown(keyCode, eDown);
988                onKeyUp(keyCode, eUp);
989            } else if (sKey.isUniStrKey()) {
990                boolean kUsed = false;
991                String keyLabel = sKey.getKeyLabel();
992                if (mInputModeSwitcher.isChineseTextWithSkb()
993                        && (ImeState.STATE_INPUT == mImeState || ImeState.STATE_COMPOSING == mImeState)) {
994                    if (mDecInfo.length() > 0 && keyLabel.length() == 1
995                            && keyLabel.charAt(0) == '\'') {
996                        processSurfaceChange('\'', 0);
997                        kUsed = true;
998                    }
999                }
1000                if (!kUsed) {
1001                    if (ImeState.STATE_INPUT == mImeState) {
1002                        commitResultText(mDecInfo
1003                                .getCurrentFullSent(mCandidatesContainer
1004                                        .getActiveCandiatePos()));
1005                    } else if (ImeState.STATE_COMPOSING == mImeState) {
1006                        commitResultText(mDecInfo.getComposingStr());
1007                    }
1008                    commitResultText(keyLabel);
1009                    resetToIdleState(false);
1010                }
1011            }
1012
1013            // If the current soft keyboard is not sticky, IME needs to go
1014            // back to the previous soft keyboard automatically.
1015            if (!mSkbContainer.isCurrentSkbSticky()) {
1016                updateIcon(mInputModeSwitcher.requestBackToPreviousSkb());
1017                resetToIdleState(false);
1018                mSkbContainer.updateInputMode();
1019            }
1020        }
1021    }
1022
1023    private void showCandidateWindow(boolean showComposingView) {
1024        if (mEnvironment.needDebug()) {
1025            Log.d(TAG, "Candidates window is shown. Parent = "
1026                    + mCandidatesContainer);
1027        }
1028
1029        setCandidatesViewShown(true);
1030
1031        if (null != mSkbContainer) mSkbContainer.requestLayout();
1032
1033        if (null == mCandidatesContainer) {
1034            resetToIdleState(false);
1035            return;
1036        }
1037
1038        updateComposingText(showComposingView);
1039        mCandidatesContainer.showCandidates(mDecInfo,
1040                ImeState.STATE_COMPOSING != mImeState);
1041        mFloatingWindowTimer.postShowFloatingWindow();
1042    }
1043
1044    private void dismissCandidateWindow() {
1045        if (mEnvironment.needDebug()) {
1046            Log.d(TAG, "Candidates window is to be dismissed");
1047        }
1048        if (null == mCandidatesContainer) return;
1049        try {
1050            mFloatingWindowTimer.cancelShowing();
1051            mFloatingWindow.dismiss();
1052        } catch (Exception e) {
1053            Log.e(TAG, "Fail to show the PopupWindow.");
1054        }
1055        setCandidatesViewShown(false);
1056
1057        if (null != mSkbContainer && mSkbContainer.isShown()) {
1058            mSkbContainer.toggleCandidateMode(false);
1059        }
1060    }
1061
1062    private void resetCandidateWindow() {
1063        if (mEnvironment.needDebug()) {
1064            Log.d(TAG, "Candidates window is to be reset");
1065        }
1066        if (null == mCandidatesContainer) return;
1067        try {
1068            mFloatingWindowTimer.cancelShowing();
1069            mFloatingWindow.dismiss();
1070        } catch (Exception e) {
1071            Log.e(TAG, "Fail to show the PopupWindow.");
1072        }
1073
1074        if (null != mSkbContainer && mSkbContainer.isShown()) {
1075            mSkbContainer.toggleCandidateMode(false);
1076        }
1077
1078        mDecInfo.resetCandidates();
1079
1080        if (null != mCandidatesContainer && mCandidatesContainer.isShown()) {
1081            showCandidateWindow(false);
1082        }
1083    }
1084
1085    private void updateIcon(int iconId) {
1086        if (iconId > 0) {
1087            showStatusIcon(iconId);
1088        } else {
1089            hideStatusIcon();
1090        }
1091    }
1092
1093    @Override
1094    public View onCreateInputView() {
1095        if (mEnvironment.needDebug()) {
1096            Log.d(TAG, "onCreateInputView.");
1097        }
1098        LayoutInflater inflater = getLayoutInflater();
1099        mSkbContainer = (SkbContainer) inflater.inflate(R.layout.skb_container,
1100                null);
1101        mSkbContainer.setService(this);
1102        mSkbContainer.setInputModeSwitcher(mInputModeSwitcher);
1103        mSkbContainer.setGestureDetector(mGestureDetectorSkb);
1104        return mSkbContainer;
1105    }
1106
1107    @Override
1108    public void onStartInput(EditorInfo editorInfo, boolean restarting) {
1109        if (mEnvironment.needDebug()) {
1110            Log.d(TAG, "onStartInput " + " ccontentType: "
1111                    + String.valueOf(editorInfo.inputType) + " Restarting:"
1112                    + String.valueOf(restarting));
1113        }
1114        updateIcon(mInputModeSwitcher.requestInputWithHkb(editorInfo));
1115        resetToIdleState(false);
1116    }
1117
1118    @Override
1119    public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
1120        if (mEnvironment.needDebug()) {
1121            Log.d(TAG, "onStartInputView " + " contentType: "
1122                    + String.valueOf(editorInfo.inputType) + " Restarting:"
1123                    + String.valueOf(restarting));
1124        }
1125        updateIcon(mInputModeSwitcher.requestInputWithSkb(editorInfo));
1126        resetToIdleState(false);
1127        mSkbContainer.updateInputMode();
1128        setCandidatesViewShown(false);
1129    }
1130
1131    @Override
1132    public void onFinishInputView(boolean finishingInput) {
1133        if (mEnvironment.needDebug()) {
1134            Log.d(TAG, "onFinishInputView.");
1135        }
1136        resetToIdleState(false);
1137        super.onFinishInputView(finishingInput);
1138    }
1139
1140    @Override
1141    public void onFinishInput() {
1142        if (mEnvironment.needDebug()) {
1143            Log.d(TAG, "onFinishInput.");
1144        }
1145        resetToIdleState(false);
1146        super.onFinishInput();
1147    }
1148
1149    @Override
1150    public void onFinishCandidatesView(boolean finishingInput) {
1151        if (mEnvironment.needDebug()) {
1152            Log.d(TAG, "onFinishCandidateView.");
1153        }
1154        resetToIdleState(false);
1155        super.onFinishCandidatesView(finishingInput);
1156    }
1157
1158    @Override public void onDisplayCompletions(CompletionInfo[] completions) {
1159        if (!isFullscreenMode()) return;
1160        if (null == completions || completions.length <= 0) return;
1161        if (null == mSkbContainer || !mSkbContainer.isShown()) return;
1162
1163        if (!mInputModeSwitcher.isChineseText() ||
1164                ImeState.STATE_IDLE == mImeState ||
1165                ImeState.STATE_PREDICT == mImeState) {
1166            mImeState = ImeState.STATE_APP_COMPLETION;
1167            mDecInfo.prepareAppCompletions(completions);
1168            showCandidateWindow(false);
1169        }
1170    }
1171
1172    private void onChoiceTouched(int activeCandNo) {
1173        if (mImeState == ImeState.STATE_COMPOSING) {
1174            changeToStateInput(true);
1175        } else if (mImeState == ImeState.STATE_INPUT
1176                || mImeState == ImeState.STATE_PREDICT) {
1177            chooseCandidate(activeCandNo);
1178        } else if (mImeState == ImeState.STATE_APP_COMPLETION) {
1179            if (null != mDecInfo.mAppCompletions && activeCandNo >= 0 &&
1180                    activeCandNo < mDecInfo.mAppCompletions.length) {
1181                CompletionInfo ci = mDecInfo.mAppCompletions[activeCandNo];
1182                if (null != ci) {
1183                    InputConnection ic = getCurrentInputConnection();
1184                    ic.commitCompletion(ci);
1185                }
1186            }
1187            resetToIdleState(false);
1188        }
1189    }
1190
1191    @Override
1192    public void requestHideSelf(int flags) {
1193        if (mEnvironment.needDebug()) {
1194            Log.d(TAG, "DimissSoftInput.");
1195        }
1196        dismissCandidateWindow();
1197        if (null != mSkbContainer && mSkbContainer.isShown()) {
1198            mSkbContainer.dismissPopups();
1199        }
1200        super.requestHideSelf(flags);
1201    }
1202
1203    public void showOptionsMenu() {
1204        AlertDialog.Builder builder = new AlertDialog.Builder(this);
1205        builder.setCancelable(true);
1206        builder.setIcon(R.drawable.app_icon);
1207        builder.setNegativeButton(android.R.string.cancel, null);
1208        CharSequence itemSettings = getString(R.string.ime_settings_activity_name);
1209        CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod);
1210        builder.setItems(new CharSequence[] {itemSettings, itemInputMethod},
1211                new DialogInterface.OnClickListener() {
1212
1213                    public void onClick(DialogInterface di, int position) {
1214                        di.dismiss();
1215                        switch (position) {
1216                        case 0:
1217                            launchSettings();
1218                            break;
1219                        case 1:
1220                            InputMethodManager.getInstance(PinyinIME.this)
1221                                    .showInputMethodPicker();
1222                            break;
1223                        }
1224                    }
1225                });
1226        builder.setTitle(getString(R.string.ime_name));
1227        mOptionsDialog = builder.create();
1228        Window window = mOptionsDialog.getWindow();
1229        WindowManager.LayoutParams lp = window.getAttributes();
1230        lp.token = mSkbContainer.getWindowToken();
1231        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
1232        window.setAttributes(lp);
1233        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
1234        mOptionsDialog.show();
1235    }
1236
1237    private void launchSettings() {
1238        Intent intent = new Intent();
1239        intent.setClass(PinyinIME.this, SettingsActivity.class);
1240        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1241        startActivity(intent);
1242    }
1243
1244    private class PopupTimer extends Handler implements Runnable {
1245        private int mParentLocation[] = new int[2];
1246
1247        void postShowFloatingWindow() {
1248            mFloatingContainer.measure(LayoutParams.WRAP_CONTENT,
1249                    LayoutParams.WRAP_CONTENT);
1250            mFloatingWindow.setWidth(mFloatingContainer.getMeasuredWidth());
1251            mFloatingWindow.setHeight(mFloatingContainer.getMeasuredHeight());
1252            post(this);
1253        }
1254
1255        void cancelShowing() {
1256            if (mFloatingWindow.isShowing()) {
1257                mFloatingWindow.dismiss();
1258            }
1259            removeCallbacks(this);
1260        }
1261
1262        public void run() {
1263            mCandidatesContainer.getLocationInWindow(mParentLocation);
1264
1265            if (!mFloatingWindow.isShowing()) {
1266                mFloatingWindow.showAtLocation(mCandidatesContainer,
1267                        Gravity.LEFT | Gravity.TOP, mParentLocation[0],
1268                        mParentLocation[1] -mFloatingWindow.getHeight());
1269            } else {
1270                mFloatingWindow
1271                .update(mParentLocation[0],
1272                        mParentLocation[1] - mFloatingWindow.getHeight(),
1273                        mFloatingWindow.getWidth(),
1274                        mFloatingWindow.getHeight());
1275            }
1276        }
1277    }
1278
1279    /**
1280     * Used to notify IME that the user selects a candidate or performs an
1281     * gesture.
1282     */
1283    public class ChoiceNotifier extends Handler implements
1284            CandidateViewListener {
1285        PinyinIME mIme;
1286
1287        ChoiceNotifier(PinyinIME ime) {
1288            mIme = ime;
1289        }
1290
1291        public void onClickChoice(int choiceId) {
1292            if (choiceId >= 0) {
1293                mIme.onChoiceTouched(choiceId);
1294            }
1295        }
1296
1297        public void onToLeftGesture() {
1298            if (ImeState.STATE_COMPOSING == mImeState) {
1299                changeToStateInput(true);
1300            }
1301            mCandidatesContainer.pageForward(true, false);
1302        }
1303
1304        public void onToRightGesture() {
1305            if (ImeState.STATE_COMPOSING == mImeState) {
1306                changeToStateInput(true);
1307            }
1308            mCandidatesContainer.pageBackward(true, false);
1309        }
1310
1311        public void onToTopGesture() {
1312        }
1313
1314        public void onToBottomGesture() {
1315        }
1316    }
1317
1318    public class OnGestureListener extends
1319            GestureDetector.SimpleOnGestureListener {
1320        /**
1321         * When user presses and drags, the minimum x-distance to make a
1322         * response to the drag event.
1323         */
1324        private static final int MIN_X_FOR_DRAG = 60;
1325
1326        /**
1327         * When user presses and drags, the minimum y-distance to make a
1328         * response to the drag event.
1329         */
1330        private static final int MIN_Y_FOR_DRAG = 40;
1331
1332        /**
1333         * Velocity threshold for a screen-move gesture. If the minimum
1334         * x-velocity is less than it, no gesture.
1335         */
1336        static private final float VELOCITY_THRESHOLD_X1 = 0.3f;
1337
1338        /**
1339         * Velocity threshold for a screen-move gesture. If the maximum
1340         * x-velocity is less than it, no gesture.
1341         */
1342        static private final float VELOCITY_THRESHOLD_X2 = 0.7f;
1343
1344        /**
1345         * Velocity threshold for a screen-move gesture. If the minimum
1346         * y-velocity is less than it, no gesture.
1347         */
1348        static private final float VELOCITY_THRESHOLD_Y1 = 0.2f;
1349
1350        /**
1351         * Velocity threshold for a screen-move gesture. If the maximum
1352         * y-velocity is less than it, no gesture.
1353         */
1354        static private final float VELOCITY_THRESHOLD_Y2 = 0.45f;
1355
1356        /** If it false, we will not response detected gestures. */
1357        private boolean mReponseGestures;
1358
1359        /** The minimum X velocity observed in the gesture. */
1360        private float mMinVelocityX = Float.MAX_VALUE;
1361
1362        /** The minimum Y velocity observed in the gesture. */
1363        private float mMinVelocityY = Float.MAX_VALUE;
1364
1365        /** The first down time for the series of touch events for an action. */
1366        private long mTimeDown;
1367
1368        /** The last time when onScroll() is called. */
1369        private long mTimeLastOnScroll;
1370
1371        /** This flag used to indicate that this gesture is not a gesture. */
1372        private boolean mNotGesture;
1373
1374        /** This flag used to indicate that this gesture has been recognized. */
1375        private boolean mGestureRecognized;
1376
1377        public OnGestureListener(boolean reponseGestures) {
1378            mReponseGestures = reponseGestures;
1379        }
1380
1381        @Override
1382        public boolean onDown(MotionEvent e) {
1383            mMinVelocityX = Integer.MAX_VALUE;
1384            mMinVelocityY = Integer.MAX_VALUE;
1385            mTimeDown = e.getEventTime();
1386            mTimeLastOnScroll = mTimeDown;
1387            mNotGesture = false;
1388            mGestureRecognized = false;
1389            return false;
1390        }
1391
1392        @Override
1393        public boolean onScroll(MotionEvent e1, MotionEvent e2,
1394                float distanceX, float distanceY) {
1395            if (mNotGesture) return false;
1396            if (mGestureRecognized) return true;
1397
1398            if (Math.abs(e1.getX() - e2.getX()) < MIN_X_FOR_DRAG
1399                    && Math.abs(e1.getY() - e2.getY()) < MIN_Y_FOR_DRAG)
1400                return false;
1401
1402            long timeNow = e2.getEventTime();
1403            long spanTotal = timeNow - mTimeDown;
1404            long spanThis = timeNow - mTimeLastOnScroll;
1405            if (0 == spanTotal) spanTotal = 1;
1406            if (0 == spanThis) spanThis = 1;
1407
1408            float vXTotal = (e2.getX() - e1.getX()) / spanTotal;
1409            float vYTotal = (e2.getY() - e1.getY()) / spanTotal;
1410
1411            // The distances are from the current point to the previous one.
1412            float vXThis = -distanceX / spanThis;
1413            float vYThis = -distanceY / spanThis;
1414
1415            float kX = vXTotal * vXThis;
1416            float kY = vYTotal * vYThis;
1417            float k1 = kX + kY;
1418            float k2 = Math.abs(kX) + Math.abs(kY);
1419
1420            if (k1 / k2 < 0.8) {
1421                mNotGesture = true;
1422                return false;
1423            }
1424            float absVXTotal = Math.abs(vXTotal);
1425            float absVYTotal = Math.abs(vYTotal);
1426            if (absVXTotal < mMinVelocityX) {
1427                mMinVelocityX = absVXTotal;
1428            }
1429            if (absVYTotal < mMinVelocityY) {
1430                mMinVelocityY = absVYTotal;
1431            }
1432
1433            if (mMinVelocityX < VELOCITY_THRESHOLD_X1
1434                    && mMinVelocityY < VELOCITY_THRESHOLD_Y1) {
1435                mNotGesture = true;
1436                return false;
1437            }
1438
1439            if (vXTotal > VELOCITY_THRESHOLD_X2
1440                    && absVYTotal < VELOCITY_THRESHOLD_Y2) {
1441                if (mReponseGestures) onDirectionGesture(Gravity.RIGHT);
1442                mGestureRecognized = true;
1443            } else if (vXTotal < -VELOCITY_THRESHOLD_X2
1444                    && absVYTotal < VELOCITY_THRESHOLD_Y2) {
1445                if (mReponseGestures) onDirectionGesture(Gravity.LEFT);
1446                mGestureRecognized = true;
1447            } else if (vYTotal > VELOCITY_THRESHOLD_Y2
1448                    && absVXTotal < VELOCITY_THRESHOLD_X2) {
1449                if (mReponseGestures) onDirectionGesture(Gravity.BOTTOM);
1450                mGestureRecognized = true;
1451            } else if (vYTotal < -VELOCITY_THRESHOLD_Y2
1452                    && absVXTotal < VELOCITY_THRESHOLD_X2) {
1453                if (mReponseGestures) onDirectionGesture(Gravity.TOP);
1454                mGestureRecognized = true;
1455            }
1456
1457            mTimeLastOnScroll = timeNow;
1458            return mGestureRecognized;
1459        }
1460
1461        @Override
1462        public boolean onFling(MotionEvent me1, MotionEvent me2,
1463                float velocityX, float velocityY) {
1464            return mGestureRecognized;
1465        }
1466
1467        public void onDirectionGesture(int gravity) {
1468            if (Gravity.NO_GRAVITY == gravity) {
1469                return;
1470            }
1471
1472            if (Gravity.LEFT == gravity || Gravity.RIGHT == gravity) {
1473                if (mCandidatesContainer.isShown()) {
1474                    if (Gravity.LEFT == gravity) {
1475                        mCandidatesContainer.pageForward(true, true);
1476                    } else {
1477                        mCandidatesContainer.pageBackward(true, true);
1478                    }
1479                    return;
1480                }
1481            }
1482        }
1483    }
1484
1485    /**
1486     * Connection used for binding to the Pinyin decoding service.
1487     */
1488    public class PinyinDecoderServiceConnection implements ServiceConnection {
1489        public void onServiceConnected(ComponentName name, IBinder service) {
1490            mDecInfo.mIPinyinDecoderService = IPinyinDecoderService.Stub
1491                    .asInterface(service);
1492        }
1493
1494        public void onServiceDisconnected(ComponentName name) {
1495        }
1496    }
1497
1498    public enum ImeState {
1499        STATE_BYPASS, STATE_IDLE, STATE_INPUT, STATE_COMPOSING, STATE_PREDICT,
1500        STATE_APP_COMPLETION
1501    }
1502
1503    public class DecodingInfo {
1504        /**
1505         * Maximum length of the Pinyin string
1506         */
1507        private static final int PY_STRING_MAX = 28;
1508
1509        /**
1510         * Maximum number of candidates to display in one page.
1511         */
1512        private static final int MAX_PAGE_SIZE_DISPLAY = 10;
1513
1514        /**
1515         * Spelling (Pinyin) string.
1516         */
1517        private StringBuffer mSurface;
1518
1519        /**
1520         * Byte buffer used as the Pinyin string parameter for native function
1521         * call.
1522         */
1523        private byte mPyBuf[];
1524
1525        /**
1526         * The length of surface string successfully decoded by engine.
1527         */
1528        private int mSurfaceDecodedLen;
1529
1530        /**
1531         * Composing string.
1532         */
1533        private String mComposingStr;
1534
1535        /**
1536         * Length of the active composing string.
1537         */
1538        private int mActiveCmpsLen;
1539
1540        /**
1541         * Composing string for display, it is copied from mComposingStr, and
1542         * add spaces between spellings.
1543         **/
1544        private String mComposingStrDisplay;
1545
1546        /**
1547         * Length of the active composing string for display.
1548         */
1549        private int mActiveCmpsDisplayLen;
1550
1551        /**
1552         * The first full sentence choice.
1553         */
1554        private String mFullSent;
1555
1556        /**
1557         * Number of characters which have been fixed.
1558         */
1559        private int mFixedLen;
1560
1561        /**
1562         * If this flag is true, selection is finished.
1563         */
1564        private boolean mFinishSelection;
1565
1566        /**
1567         * The starting position for each spelling. The first one is the number
1568         * of the real starting position elements.
1569         */
1570        private int mSplStart[];
1571
1572        /**
1573         * Editing cursor in mSurface.
1574         */
1575        private int mCursorPos;
1576
1577        /**
1578         * Remote Pinyin-to-Hanzi decoding engine service.
1579         */
1580        private IPinyinDecoderService mIPinyinDecoderService;
1581
1582        /**
1583         * The complication information suggested by application.
1584         */
1585        private CompletionInfo[] mAppCompletions;
1586
1587        /**
1588         * The total number of choices for display. The list may only contains
1589         * the first part. If user tries to navigate to next page which is not
1590         * in the result list, we need to get these items.
1591         **/
1592        public int mTotalChoicesNum;
1593
1594        /**
1595         * Candidate list. The first one is the full-sentence candidate.
1596         */
1597        public List<String> mCandidatesList = new Vector<String>();
1598
1599        /**
1600         * Element i stores the starting position of page i.
1601         */
1602        public Vector<Integer> mPageStart = new Vector<Integer>();
1603
1604        /**
1605         * Element i stores the number of characters to page i.
1606         */
1607        public Vector<Integer> mCnToPage = new Vector<Integer>();
1608
1609        /**
1610         * The position to delete in Pinyin string. If it is less than 0, IME
1611         * will do an incremental search, otherwise IME will do a deletion
1612         * operation. if {@link #mIsPosInSpl} is true, IME will delete the whole
1613         * string for mPosDelSpl-th spelling, otherwise it will only delete
1614         * mPosDelSpl-th character in the Pinyin string.
1615         */
1616        public int mPosDelSpl = -1;
1617
1618        /**
1619         * If {@link #mPosDelSpl} is big than or equal to 0, this member is used
1620         * to indicate that whether the postion is counted in spelling id or
1621         * character.
1622         */
1623        public boolean mIsPosInSpl;
1624
1625        public DecodingInfo() {
1626            mSurface = new StringBuffer();
1627            mSurfaceDecodedLen = 0;
1628        }
1629
1630        public void reset() {
1631            mSurface.delete(0, mSurface.length());
1632            mSurfaceDecodedLen = 0;
1633            mCursorPos = 0;
1634            mFullSent = "";
1635            mFixedLen = 0;
1636            mFinishSelection = false;
1637            mComposingStr = "";
1638            mComposingStrDisplay = "";
1639            mActiveCmpsLen = 0;
1640            mActiveCmpsDisplayLen = 0;
1641
1642            resetCandidates();
1643        }
1644
1645        public boolean isCandidatesListEmpty() {
1646            return mCandidatesList.size() == 0;
1647        }
1648
1649        public boolean isSplStrFull() {
1650            if (mSurface.length() >= PY_STRING_MAX - 1) return true;
1651            return false;
1652        }
1653
1654        public void addSplChar(char ch, boolean reset) {
1655            if (reset) {
1656                mSurface.delete(0, mSurface.length());
1657                mSurfaceDecodedLen = 0;
1658                mCursorPos = 0;
1659                try {
1660                    mIPinyinDecoderService.imResetSearch();
1661                } catch (RemoteException e) {
1662                }
1663            }
1664            mSurface.insert(mCursorPos, ch);
1665            mCursorPos++;
1666        }
1667
1668        // Prepare to delete before cursor. We may delete a spelling char if
1669        // the cursor is in the range of unfixed part, delete a whole spelling
1670        // if the cursor in inside the range of the fixed part.
1671        // This function only marks the position used to delete.
1672        public void prepareDeleteBeforeCursor() {
1673            if (mCursorPos > 0) {
1674                int pos;
1675                for (pos = 0; pos < mFixedLen; pos++) {
1676                    if (mSplStart[pos + 2] >= mCursorPos
1677                            && mSplStart[pos + 1] < mCursorPos) {
1678                        mPosDelSpl = pos;
1679                        mCursorPos = mSplStart[pos + 1];
1680                        mIsPosInSpl = true;
1681                        break;
1682                    }
1683                }
1684                if (mPosDelSpl < 0) {
1685                    mPosDelSpl = mCursorPos - 1;
1686                    mCursorPos--;
1687                    mIsPosInSpl = false;
1688                }
1689            }
1690        }
1691
1692        public int length() {
1693            return mSurface.length();
1694        }
1695
1696        public char charAt(int index) {
1697            return mSurface.charAt(index);
1698        }
1699
1700        public StringBuffer getOrigianlSplStr() {
1701            return mSurface;
1702        }
1703
1704        public int getSplStrDecodedLen() {
1705            return mSurfaceDecodedLen;
1706        }
1707
1708        public int[] getSplStart() {
1709            return mSplStart;
1710        }
1711
1712        public String getComposingStr() {
1713            return mComposingStr;
1714        }
1715
1716        public String getComposingStrActivePart() {
1717            assert (mActiveCmpsLen <= mComposingStr.length());
1718            return mComposingStr.substring(0, mActiveCmpsLen);
1719        }
1720
1721        public int getActiveCmpsLen() {
1722            return mActiveCmpsLen;
1723        }
1724
1725        public String getComposingStrForDisplay() {
1726            return mComposingStrDisplay;
1727        }
1728
1729        public int getActiveCmpsDisplayLen() {
1730            return mActiveCmpsDisplayLen;
1731        }
1732
1733        public String getFullSent() {
1734            return mFullSent;
1735        }
1736
1737        public String getCurrentFullSent(int activeCandPos) {
1738            try {
1739                String retStr = mFullSent.substring(0, mFixedLen);
1740                retStr += mCandidatesList.get(activeCandPos);
1741                return retStr;
1742            } catch (Exception e) {
1743                return "";
1744            }
1745        }
1746
1747        public void resetCandidates() {
1748            mCandidatesList.clear();
1749            mTotalChoicesNum = 0;
1750
1751            mPageStart.clear();
1752            mPageStart.add(0);
1753            mCnToPage.clear();
1754            mCnToPage.add(0);
1755        }
1756
1757        public boolean candidatesFromApp() {
1758            return ImeState.STATE_APP_COMPLETION == mImeState;
1759        }
1760
1761        public boolean canDoPrediction() {
1762            return mComposingStr.length() == mFixedLen;
1763        }
1764
1765        public boolean selectionFinished() {
1766            return mFinishSelection;
1767        }
1768
1769        // After the user chooses a candidate, input method will do a
1770        // re-decoding and give the new candidate list.
1771        // If candidate id is less than 0, means user is inputting Pinyin,
1772        // not selecting any choice.
1773        private void chooseDecodingCandidate(int candId) {
1774            if (mImeState != ImeState.STATE_PREDICT) {
1775                resetCandidates();
1776                int totalChoicesNum = 0;
1777                try {
1778                    if (candId < 0) {
1779                        if (length() == 0) {
1780                            totalChoicesNum = 0;
1781                        } else {
1782                            if (mPyBuf == null)
1783                                mPyBuf = new byte[PY_STRING_MAX];
1784                            for (int i = 0; i < length(); i++)
1785                                mPyBuf[i] = (byte) charAt(i);
1786                            mPyBuf[length()] = 0;
1787
1788                            if (mPosDelSpl < 0) {
1789                                totalChoicesNum = mIPinyinDecoderService
1790                                        .imSearch(mPyBuf, length());
1791                            } else {
1792                                boolean clear_fixed_this_step = true;
1793                                if (ImeState.STATE_COMPOSING == mImeState) {
1794                                    clear_fixed_this_step = false;
1795                                }
1796                                totalChoicesNum = mIPinyinDecoderService
1797                                        .imDelSearch(mPosDelSpl, mIsPosInSpl,
1798                                                clear_fixed_this_step);
1799                                mPosDelSpl = -1;
1800                            }
1801                        }
1802                    } else {
1803                        totalChoicesNum = mIPinyinDecoderService
1804                                .imChoose(candId);
1805                    }
1806                } catch (RemoteException e) {
1807                }
1808                updateDecInfoForSearch(totalChoicesNum);
1809            }
1810        }
1811
1812        private void updateDecInfoForSearch(int totalChoicesNum) {
1813            mTotalChoicesNum = totalChoicesNum;
1814            if (mTotalChoicesNum < 0) {
1815                mTotalChoicesNum = 0;
1816                return;
1817            }
1818
1819            try {
1820                String pyStr;
1821
1822                mSplStart = mIPinyinDecoderService.imGetSplStart();
1823                pyStr = mIPinyinDecoderService.imGetPyStr(false);
1824                mSurfaceDecodedLen = mIPinyinDecoderService.imGetPyStrLen(true);
1825                assert (mSurfaceDecodedLen <= pyStr.length());
1826
1827                mFullSent = mIPinyinDecoderService.imGetChoice(0);
1828                mFixedLen = mIPinyinDecoderService.imGetFixedLen();
1829
1830                // Update the surface string to the one kept by engine.
1831                mSurface.replace(0, mSurface.length(), pyStr);
1832
1833                if (mCursorPos > mSurface.length())
1834                    mCursorPos = mSurface.length();
1835                mComposingStr = mFullSent.substring(0, mFixedLen)
1836                        + mSurface.substring(mSplStart[mFixedLen + 1]);
1837
1838                mActiveCmpsLen = mComposingStr.length();
1839                if (mSurfaceDecodedLen > 0) {
1840                    mActiveCmpsLen = mActiveCmpsLen
1841                            - (mSurface.length() - mSurfaceDecodedLen);
1842                }
1843
1844                // Prepare the display string.
1845                if (0 == mSurfaceDecodedLen) {
1846                    mComposingStrDisplay = mComposingStr;
1847                    mActiveCmpsDisplayLen = mComposingStr.length();
1848                } else {
1849                    mComposingStrDisplay = mFullSent.substring(0, mFixedLen);
1850                    for (int pos = mFixedLen + 1; pos < mSplStart.length - 1; pos++) {
1851                        mComposingStrDisplay += mSurface.substring(
1852                                mSplStart[pos], mSplStart[pos + 1]);
1853                        if (mSplStart[pos + 1] < mSurfaceDecodedLen) {
1854                            mComposingStrDisplay += " ";
1855                        }
1856                    }
1857                    mActiveCmpsDisplayLen = mComposingStrDisplay.length();
1858                    if (mSurfaceDecodedLen < mSurface.length()) {
1859                        mComposingStrDisplay += mSurface
1860                                .substring(mSurfaceDecodedLen);
1861                    }
1862                }
1863
1864                if (mSplStart.length == mFixedLen + 2) {
1865                    mFinishSelection = true;
1866                } else {
1867                    mFinishSelection = false;
1868                }
1869            } catch (RemoteException e) {
1870                Log.w(TAG, "PinyinDecoderService died", e);
1871            } catch (Exception e) {
1872                mTotalChoicesNum = 0;
1873                mComposingStr = "";
1874            }
1875            // Prepare page 0.
1876            if (!mFinishSelection) {
1877                preparePage(0);
1878            }
1879        }
1880
1881        private void choosePredictChoice(int choiceId) {
1882            if (ImeState.STATE_PREDICT != mImeState || choiceId < 0
1883                    || choiceId >= mTotalChoicesNum) {
1884                return;
1885            }
1886
1887            String tmp = mCandidatesList.get(choiceId);
1888
1889            resetCandidates();
1890
1891            mCandidatesList.add(tmp);
1892            mTotalChoicesNum = 1;
1893
1894            mSurface.replace(0, mSurface.length(), "");
1895            mCursorPos = 0;
1896            mFullSent = tmp;
1897            mFixedLen = tmp.length();
1898            mComposingStr = mFullSent;
1899            mActiveCmpsLen = mFixedLen;
1900
1901            mFinishSelection = true;
1902        }
1903
1904        public String getCandidate(int candId) {
1905            // Only loaded items can be gotten, so we use mCandidatesList.size()
1906            // instead mTotalChoiceNum.
1907            if (candId < 0 || candId > mCandidatesList.size()) {
1908                return null;
1909            }
1910            return mCandidatesList.get(candId);
1911        }
1912
1913        private void getCandiagtesForCache() {
1914            int fetchStart = mCandidatesList.size();
1915            int fetchSize = mTotalChoicesNum - fetchStart;
1916            if (fetchSize > MAX_PAGE_SIZE_DISPLAY) {
1917                fetchSize = MAX_PAGE_SIZE_DISPLAY;
1918            }
1919            try {
1920                List<String> newList = null;
1921                if (ImeState.STATE_INPUT == mImeState ||
1922                        ImeState.STATE_IDLE == mImeState ||
1923                        ImeState.STATE_COMPOSING == mImeState){
1924                    newList = mIPinyinDecoderService.imGetChoiceList(
1925                            fetchStart, fetchSize, mFixedLen);
1926                } else if (ImeState.STATE_PREDICT == mImeState) {
1927                    newList = mIPinyinDecoderService.imGetPredictList(
1928                            fetchStart, fetchSize);
1929                } else if (ImeState.STATE_APP_COMPLETION == mImeState) {
1930                    newList = new ArrayList<String>();
1931                    if (null != mAppCompletions) {
1932                        for (int pos = fetchStart; pos < fetchSize; pos++) {
1933                            CompletionInfo ci = mAppCompletions[pos];
1934                            if (null != ci) {
1935                                CharSequence s = ci.getText();
1936                                if (null != s) newList.add(s.toString());
1937                            }
1938                        }
1939                    }
1940                }
1941                mCandidatesList.addAll(newList);
1942            } catch (RemoteException e) {
1943                Log.w(TAG, "PinyinDecoderService died", e);
1944            }
1945        }
1946
1947        public boolean pageReady(int pageNo) {
1948            // If the page number is less than 0, return false
1949            if (pageNo < 0) return false;
1950
1951            // Page pageNo's ending information is not ready.
1952            if (mPageStart.size() <= pageNo + 1) {
1953                return false;
1954            }
1955
1956            return true;
1957        }
1958
1959        public boolean preparePage(int pageNo) {
1960            // If the page number is less than 0, return false
1961            if (pageNo < 0) return false;
1962
1963            // Make sure the starting information for page pageNo is ready.
1964            if (mPageStart.size() <= pageNo) {
1965                return false;
1966            }
1967
1968            // Page pageNo's ending information is also ready.
1969            if (mPageStart.size() > pageNo + 1) {
1970                return true;
1971            }
1972
1973            // If cached items is enough for page pageNo.
1974            if (mCandidatesList.size() - mPageStart.elementAt(pageNo) >= MAX_PAGE_SIZE_DISPLAY) {
1975                return true;
1976            }
1977
1978            // Try to get more items from engine
1979            getCandiagtesForCache();
1980
1981            // Try to find if there are available new items to display.
1982            // If no new item, return false;
1983            if (mPageStart.elementAt(pageNo) >= mCandidatesList.size()) {
1984                return false;
1985            }
1986
1987            // If there are new items, return true;
1988            return true;
1989        }
1990
1991        public void preparePredicts(CharSequence history) {
1992            if (null == history) return;
1993
1994            resetCandidates();
1995
1996            if (Settings.getPrediction()) {
1997                String preEdit = history.toString();
1998                int predictNum = 0;
1999                if (null != preEdit) {
2000                    try {
2001                        mTotalChoicesNum = mIPinyinDecoderService
2002                                .imGetPredictsNum(preEdit);
2003                    } catch (RemoteException e) {
2004                        return;
2005                    }
2006                }
2007            }
2008
2009            preparePage(0);
2010            mFinishSelection = false;
2011        }
2012
2013        private void prepareAppCompletions(CompletionInfo completions[]) {
2014            resetCandidates();
2015            mAppCompletions = completions;
2016            mTotalChoicesNum = completions.length;
2017            preparePage(0);
2018            mFinishSelection = false;
2019            return;
2020        }
2021
2022        public int getCurrentPageSize(int currentPage) {
2023            if (mPageStart.size() <= currentPage + 1) return 0;
2024            return mPageStart.elementAt(currentPage + 1)
2025                    - mPageStart.elementAt(currentPage);
2026        }
2027
2028        public int getCurrentPageStart(int currentPage) {
2029            if (mPageStart.size() < currentPage + 1) return mTotalChoicesNum;
2030            return mPageStart.elementAt(currentPage);
2031        }
2032
2033        public boolean pageForwardable(int currentPage) {
2034            if (mPageStart.size() <= currentPage + 1) return false;
2035            if (mPageStart.elementAt(currentPage + 1) >= mTotalChoicesNum) {
2036                return false;
2037            }
2038            return true;
2039        }
2040
2041        public boolean pageBackwardable(int currentPage) {
2042            if (currentPage > 0) return true;
2043            return false;
2044        }
2045
2046        public boolean charBeforeCursorIsSeparator() {
2047            int len = mSurface.length();
2048            if (mCursorPos > len) return false;
2049            if (mCursorPos > 0 && mSurface.charAt(mCursorPos - 1) == '\'') {
2050                return true;
2051            }
2052            return false;
2053        }
2054
2055        public int getCursorPos() {
2056            return mCursorPos;
2057        }
2058
2059        public int getCursorPosInCmps() {
2060            int cursorPos = mCursorPos;
2061            int fixedLen = 0;
2062
2063            for (int hzPos = 0; hzPos < mFixedLen; hzPos++) {
2064                if (mCursorPos >= mSplStart[hzPos + 2]) {
2065                    cursorPos -= mSplStart[hzPos + 2] - mSplStart[hzPos + 1];
2066                    cursorPos += 1;
2067                }
2068            }
2069            return cursorPos;
2070        }
2071
2072        public int getCursorPosInCmpsDisplay() {
2073            int cursorPos = getCursorPosInCmps();
2074            // +2 is because: one for mSplStart[0], which is used for other
2075            // purpose(The length of the segmentation string), and another
2076            // for the first spelling which does not need a space before it.
2077            for (int pos = mFixedLen + 2; pos < mSplStart.length - 1; pos++) {
2078                if (mCursorPos <= mSplStart[pos]) {
2079                    break;
2080                } else {
2081                    cursorPos++;
2082                }
2083            }
2084            return cursorPos;
2085        }
2086
2087        public void moveCursorToEdge(boolean left) {
2088            if (left)
2089                mCursorPos = 0;
2090            else
2091                mCursorPos = mSurface.length();
2092        }
2093
2094        // Move cursor. If offset is 0, this function can be used to adjust
2095        // the cursor into the bounds of the string.
2096        public void moveCursor(int offset) {
2097            if (offset > 1 || offset < -1) return;
2098
2099            if (offset != 0) {
2100                int hzPos = 0;
2101                for (hzPos = 0; hzPos <= mFixedLen; hzPos++) {
2102                    if (mCursorPos == mSplStart[hzPos + 1]) {
2103                        if (offset < 0) {
2104                            if (hzPos > 0) {
2105                                offset = mSplStart[hzPos]
2106                                        - mSplStart[hzPos + 1];
2107                            }
2108                        } else {
2109                            if (hzPos < mFixedLen) {
2110                                offset = mSplStart[hzPos + 2]
2111                                        - mSplStart[hzPos + 1];
2112                            }
2113                        }
2114                        break;
2115                    }
2116                }
2117            }
2118            mCursorPos += offset;
2119            if (mCursorPos < 0) {
2120                mCursorPos = 0;
2121            } else if (mCursorPos > mSurface.length()) {
2122                mCursorPos = mSurface.length();
2123            }
2124        }
2125
2126        public int getSplNum() {
2127            return mSplStart[0];
2128        }
2129
2130        public int getFixedLen() {
2131            return mFixedLen;
2132        }
2133    }
2134}
2135