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 17 package com.android.inputmethod.pinyin; 18 19 import android.app.AlertDialog; 20 import android.content.BroadcastReceiver; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.Intent; 25 import android.content.ServiceConnection; 26 import android.content.res.Configuration; 27 import android.inputmethodservice.InputMethodService; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.preference.PreferenceManager; 32 import android.util.Log; 33 import android.view.Gravity; 34 import android.view.GestureDetector; 35 import android.view.LayoutInflater; 36 import android.view.KeyEvent; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.Window; 40 import android.view.WindowManager; 41 import android.view.View.MeasureSpec; 42 import android.view.ViewGroup.LayoutParams; 43 import android.view.inputmethod.CompletionInfo; 44 import android.view.inputmethod.InputConnection; 45 import android.view.inputmethod.EditorInfo; 46 import android.view.inputmethod.InputMethodManager; 47 import android.widget.LinearLayout; 48 import android.widget.PopupWindow; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Vector; 53 54 /** 55 * Main class of the Pinyin input method. 56 */ 57 public 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() 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