1 /* 2 * Copyright (C) 2008-2009 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.latin; 18 19 import java.util.List; 20 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Paint; 24 import android.inputmethodservice.Keyboard; 25 import android.inputmethodservice.KeyboardView; 26 import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; 27 import android.inputmethodservice.Keyboard.Key; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.os.SystemClock; 31 import android.util.AttributeSet; 32 import android.view.LayoutInflater; 33 import android.view.MotionEvent; 34 import android.widget.PopupWindow; 35 36 public class LatinKeyboardView extends KeyboardView { 37 38 static final int KEYCODE_OPTIONS = -100; 39 static final int KEYCODE_SHIFT_LONGPRESS = -101; 40 static final int KEYCODE_VOICE = -102; 41 static final int KEYCODE_F1 = -103; 42 static final int KEYCODE_NEXT_LANGUAGE = -104; 43 static final int KEYCODE_PREV_LANGUAGE = -105; 44 45 private Keyboard mPhoneKeyboard; 46 47 /** Whether the extension of this keyboard is visible */ 48 private boolean mExtensionVisible; 49 /** The view that is shown as an extension of this keyboard view */ 50 private LatinKeyboardView mExtension; 51 /** The popup window that contains the extension of this keyboard */ 52 private PopupWindow mExtensionPopup; 53 /** Whether this view is an extension of another keyboard */ 54 private boolean mIsExtensionType; 55 private boolean mFirstEvent; 56 /** Whether we've started dropping move events because we found a big jump */ 57 private boolean mDroppingEvents; 58 /** 59 * Whether multi-touch disambiguation needs to be disabled for any reason. There are 2 reasons 60 * for this to happen - (1) if a real multi-touch event has occured and (2) we've opened an 61 * extension keyboard. 62 */ 63 private boolean mDisableDisambiguation; 64 /** The distance threshold at which we start treating the touch session as a multi-touch */ 65 private int mJumpThresholdSquare = Integer.MAX_VALUE; 66 /** The y coordinate of the last row */ 67 private int mLastRowY; 68 69 public LatinKeyboardView(Context context, AttributeSet attrs) { 70 super(context, attrs); 71 } 72 73 public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) { 74 super(context, attrs, defStyle); 75 } 76 77 public void setPhoneKeyboard(Keyboard phoneKeyboard) { 78 mPhoneKeyboard = phoneKeyboard; 79 } 80 81 @Override 82 public void setKeyboard(Keyboard k) { 83 super.setKeyboard(k); 84 // One-seventh of the keyboard width seems like a reasonable threshold 85 mJumpThresholdSquare = k.getMinWidth() / 7; 86 mJumpThresholdSquare *= mJumpThresholdSquare; 87 // Assuming there are 4 rows, this is the coordinate of the last row 88 mLastRowY = (k.getHeight() * 3) / 4; 89 setKeyboardLocal(k); 90 } 91 92 @Override 93 protected boolean onLongPress(Key key) { 94 if (key.codes[0] == Keyboard.KEYCODE_MODE_CHANGE) { 95 getOnKeyboardActionListener().onKey(KEYCODE_OPTIONS, null); 96 return true; 97 } else if (key.codes[0] == Keyboard.KEYCODE_SHIFT) { 98 getOnKeyboardActionListener().onKey(KEYCODE_SHIFT_LONGPRESS, null); 99 invalidateAllKeys(); 100 return true; 101 } else if (key.codes[0] == '0' && getKeyboard() == mPhoneKeyboard) { 102 // Long pressing on 0 in phone number keypad gives you a '+'. 103 getOnKeyboardActionListener().onKey('+', null); 104 return true; 105 } else { 106 return super.onLongPress(key); 107 } 108 } 109 110 /** 111 * This function checks to see if we need to handle any sudden jumps in the pointer location 112 * that could be due to a multi-touch being treated as a move by the firmware or hardware. 113 * Once a sudden jump is detected, all subsequent move events are discarded 114 * until an UP is received.<P> 115 * When a sudden jump is detected, an UP event is simulated at the last position and when 116 * the sudden moves subside, a DOWN event is simulated for the second key. 117 * @param me the motion event 118 * @return true if the event was consumed, so that it doesn't continue to be handled by 119 * KeyboardView. 120 */ 121 private boolean handleSuddenJump(MotionEvent me) { 122 final int action = me.getAction(); 123 final int x = (int) me.getX(); 124 final int y = (int) me.getY(); 125 boolean result = false; 126 127 // Real multi-touch event? Stop looking for sudden jumps 128 if (me.getPointerCount() > 1) { 129 mDisableDisambiguation = true; 130 } 131 if (mDisableDisambiguation) { 132 // If UP, reset the multi-touch flag 133 if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false; 134 return false; 135 } 136 137 switch (action) { 138 case MotionEvent.ACTION_DOWN: 139 // Reset the "session" 140 mDroppingEvents = false; 141 mDisableDisambiguation = false; 142 break; 143 case MotionEvent.ACTION_MOVE: 144 // Is this a big jump? 145 final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y); 146 // Check the distance and also if the move is not entirely within the bottom row 147 // If it's only in the bottom row, it might be an intentional slide gesture 148 // for language switching 149 if (distanceSquare > mJumpThresholdSquare 150 && (mLastY < mLastRowY || y < mLastRowY)) { 151 // If we're not yet dropping events, start dropping and send an UP event 152 if (!mDroppingEvents) { 153 mDroppingEvents = true; 154 // Send an up event 155 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 156 MotionEvent.ACTION_UP, 157 mLastX, mLastY, me.getMetaState()); 158 super.onTouchEvent(translated); 159 translated.recycle(); 160 } 161 result = true; 162 } else if (mDroppingEvents) { 163 // If moves are small and we're already dropping events, continue dropping 164 result = true; 165 } 166 break; 167 case MotionEvent.ACTION_UP: 168 if (mDroppingEvents) { 169 // Send a down event first, as we dropped a bunch of sudden jumps and assume that 170 // the user is releasing the touch on the second key. 171 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 172 MotionEvent.ACTION_DOWN, 173 x, y, me.getMetaState()); 174 super.onTouchEvent(translated); 175 translated.recycle(); 176 mDroppingEvents = false; 177 // Let the up event get processed as well, result = false 178 } 179 break; 180 } 181 // Track the previous coordinate 182 mLastX = x; 183 mLastY = y; 184 return result; 185 } 186 187 @Override 188 public boolean onTouchEvent(MotionEvent me) { 189 LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); 190 if (DEBUG_LINE) { 191 mLastX = (int) me.getX(); 192 mLastY = (int) me.getY(); 193 invalidate(); 194 } 195 196 // If an extension keyboard is visible or this is an extension keyboard, don't look 197 // for sudden jumps. Otherwise, if there was a sudden jump, return without processing the 198 // actual motion event. 199 if (!mExtensionVisible && !mIsExtensionType 200 && handleSuddenJump(me)) return true; 201 // Reset any bounding box controls in the keyboard 202 if (me.getAction() == MotionEvent.ACTION_DOWN) { 203 keyboard.keyReleased(); 204 } 205 206 if (me.getAction() == MotionEvent.ACTION_UP) { 207 int languageDirection = keyboard.getLanguageChangeDirection(); 208 if (languageDirection != 0) { 209 getOnKeyboardActionListener().onKey( 210 languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE, 211 null); 212 me.setAction(MotionEvent.ACTION_CANCEL); 213 keyboard.keyReleased(); 214 return super.onTouchEvent(me); 215 } 216 } 217 218 // If we don't have an extension keyboard, don't go any further. 219 if (keyboard.getExtension() == 0) { 220 return super.onTouchEvent(me); 221 } 222 // If the motion event is above the keyboard and it's not an UP event coming 223 // even before the first MOVE event into the extension area 224 if (me.getY() < 0 && (mExtensionVisible || me.getAction() != MotionEvent.ACTION_UP)) { 225 if (mExtensionVisible) { 226 int action = me.getAction(); 227 if (mFirstEvent) action = MotionEvent.ACTION_DOWN; 228 mFirstEvent = false; 229 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 230 action, 231 me.getX(), me.getY() + mExtension.getHeight(), me.getMetaState()); 232 boolean result = mExtension.onTouchEvent(translated); 233 translated.recycle(); 234 if (me.getAction() == MotionEvent.ACTION_UP 235 || me.getAction() == MotionEvent.ACTION_CANCEL) { 236 closeExtension(); 237 } 238 return result; 239 } else { 240 if (openExtension()) { 241 MotionEvent cancel = MotionEvent.obtain(me.getDownTime(), me.getEventTime(), 242 MotionEvent.ACTION_CANCEL, me.getX() - 100, me.getY() - 100, 0); 243 super.onTouchEvent(cancel); 244 cancel.recycle(); 245 if (mExtension.getHeight() > 0) { 246 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), 247 me.getEventTime(), 248 MotionEvent.ACTION_DOWN, 249 me.getX(), me.getY() + mExtension.getHeight(), 250 me.getMetaState()); 251 mExtension.onTouchEvent(translated); 252 translated.recycle(); 253 } else { 254 mFirstEvent = true; 255 } 256 // Stop processing multi-touch errors 257 mDisableDisambiguation = true; 258 } 259 return true; 260 } 261 } else if (mExtensionVisible) { 262 closeExtension(); 263 // Send a down event into the main keyboard first 264 MotionEvent down = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 265 MotionEvent.ACTION_DOWN, 266 me.getX(), me.getY(), me.getMetaState()); 267 super.onTouchEvent(down); 268 down.recycle(); 269 // Send the actual event 270 return super.onTouchEvent(me); 271 } else { 272 return super.onTouchEvent(me); 273 } 274 } 275 276 private void setExtensionType(boolean isExtensionType) { 277 mIsExtensionType = isExtensionType; 278 } 279 280 private boolean openExtension() { 281 // If the current keyboard is not visible, don't show the popup 282 if (!isShown()) { 283 return false; 284 } 285 if (((LatinKeyboard) getKeyboard()).getExtension() == 0) return false; 286 makePopupWindow(); 287 mExtensionVisible = true; 288 return true; 289 } 290 291 private void makePopupWindow() { 292 if (mExtensionPopup == null) { 293 int[] windowLocation = new int[2]; 294 mExtensionPopup = new PopupWindow(getContext()); 295 mExtensionPopup.setBackgroundDrawable(null); 296 LayoutInflater li = (LayoutInflater) getContext().getSystemService( 297 Context.LAYOUT_INFLATER_SERVICE); 298 mExtension = (LatinKeyboardView) li.inflate(R.layout.input_trans, null); 299 mExtension.setExtensionType(true); 300 mExtension.setOnKeyboardActionListener( 301 new ExtensionKeyboardListener(getOnKeyboardActionListener())); 302 mExtension.setPopupParent(this); 303 mExtension.setPopupOffset(0, -windowLocation[1]); 304 Keyboard keyboard; 305 mExtension.setKeyboard(keyboard = new LatinKeyboard(getContext(), 306 ((LatinKeyboard) getKeyboard()).getExtension())); 307 mExtensionPopup.setContentView(mExtension); 308 mExtensionPopup.setWidth(getWidth()); 309 mExtensionPopup.setHeight(keyboard.getHeight()); 310 mExtensionPopup.setAnimationStyle(-1); 311 getLocationInWindow(windowLocation); 312 // TODO: Fix the "- 30". 313 mExtension.setPopupOffset(0, -windowLocation[1] - 30); 314 mExtensionPopup.showAtLocation(this, 0, 0, -keyboard.getHeight() 315 + windowLocation[1]); 316 } else { 317 mExtension.setVisibility(VISIBLE); 318 } 319 } 320 321 @Override 322 public void closing() { 323 super.closing(); 324 if (mExtensionPopup != null && mExtensionPopup.isShowing()) { 325 mExtensionPopup.dismiss(); 326 mExtensionPopup = null; 327 } 328 } 329 330 private void closeExtension() { 331 mExtension.closing(); 332 mExtension.setVisibility(INVISIBLE); 333 mExtensionVisible = false; 334 } 335 336 private static class ExtensionKeyboardListener implements OnKeyboardActionListener { 337 private OnKeyboardActionListener mTarget; 338 ExtensionKeyboardListener(OnKeyboardActionListener target) { 339 mTarget = target; 340 } 341 public void onKey(int primaryCode, int[] keyCodes) { 342 mTarget.onKey(primaryCode, keyCodes); 343 } 344 public void onPress(int primaryCode) { 345 mTarget.onPress(primaryCode); 346 } 347 public void onRelease(int primaryCode) { 348 mTarget.onRelease(primaryCode); 349 } 350 public void onText(CharSequence text) { 351 mTarget.onText(text); 352 } 353 public void swipeDown() { 354 // Don't pass through 355 } 356 public void swipeLeft() { 357 // Don't pass through 358 } 359 public void swipeRight() { 360 // Don't pass through 361 } 362 public void swipeUp() { 363 // Don't pass through 364 } 365 } 366 367 /**************************** INSTRUMENTATION *******************************/ 368 369 static final boolean DEBUG_AUTO_PLAY = false; 370 static final boolean DEBUG_LINE = false; 371 private static final int MSG_TOUCH_DOWN = 1; 372 private static final int MSG_TOUCH_UP = 2; 373 374 Handler mHandler2; 375 376 private String mStringToPlay; 377 private int mStringIndex; 378 private boolean mDownDelivered; 379 private Key[] mAsciiKeys = new Key[256]; 380 private boolean mPlaying; 381 private int mLastX; 382 private int mLastY; 383 private Paint mPaint; 384 385 private void setKeyboardLocal(Keyboard k) { 386 if (DEBUG_AUTO_PLAY) { 387 findKeys(); 388 if (mHandler2 == null) { 389 mHandler2 = new Handler() { 390 @Override 391 public void handleMessage(Message msg) { 392 removeMessages(MSG_TOUCH_DOWN); 393 removeMessages(MSG_TOUCH_UP); 394 if (mPlaying == false) return; 395 396 switch (msg.what) { 397 case MSG_TOUCH_DOWN: 398 if (mStringIndex >= mStringToPlay.length()) { 399 mPlaying = false; 400 return; 401 } 402 char c = mStringToPlay.charAt(mStringIndex); 403 while (c > 255 || mAsciiKeys[(int) c] == null) { 404 mStringIndex++; 405 if (mStringIndex >= mStringToPlay.length()) { 406 mPlaying = false; 407 return; 408 } 409 c = mStringToPlay.charAt(mStringIndex); 410 } 411 int x = mAsciiKeys[c].x + 10; 412 int y = mAsciiKeys[c].y + 26; 413 MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(), 414 SystemClock.uptimeMillis(), 415 MotionEvent.ACTION_DOWN, x, y, 0); 416 LatinKeyboardView.this.dispatchTouchEvent(me); 417 me.recycle(); 418 sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else 419 // happens 420 mDownDelivered = true; 421 break; 422 case MSG_TOUCH_UP: 423 char cUp = mStringToPlay.charAt(mStringIndex); 424 int x2 = mAsciiKeys[cUp].x + 10; 425 int y2 = mAsciiKeys[cUp].y + 26; 426 mStringIndex++; 427 428 MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(), 429 SystemClock.uptimeMillis(), 430 MotionEvent.ACTION_UP, x2, y2, 0); 431 LatinKeyboardView.this.dispatchTouchEvent(me2); 432 me2.recycle(); 433 sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else 434 // happens 435 mDownDelivered = false; 436 break; 437 } 438 } 439 }; 440 441 } 442 } 443 } 444 445 private void findKeys() { 446 List<Key> keys = getKeyboard().getKeys(); 447 // Get the keys on this keyboard 448 for (int i = 0; i < keys.size(); i++) { 449 int code = keys.get(i).codes[0]; 450 if (code >= 0 && code <= 255) { 451 mAsciiKeys[code] = keys.get(i); 452 } 453 } 454 } 455 456 void startPlaying(String s) { 457 if (!DEBUG_AUTO_PLAY) return; 458 if (s == null) return; 459 mStringToPlay = s.toLowerCase(); 460 mPlaying = true; 461 mDownDelivered = false; 462 mStringIndex = 0; 463 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); 464 } 465 466 @Override 467 public void draw(Canvas c) { 468 super.draw(c); 469 if (DEBUG_AUTO_PLAY && mPlaying) { 470 mHandler2.removeMessages(MSG_TOUCH_DOWN); 471 mHandler2.removeMessages(MSG_TOUCH_UP); 472 if (mDownDelivered) { 473 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); 474 } else { 475 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); 476 } 477 } 478 if (DEBUG_LINE) { 479 if (mPaint == null) { 480 mPaint = new Paint(); 481 mPaint.setColor(0x80FFFFFF); 482 mPaint.setAntiAlias(false); 483 } 484 c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint); 485 c.drawLine(0, mLastY, getWidth(), mLastY, mPaint); 486 } 487 } 488 } 489