1 /* 2 * Copyright (C) 2008 The Android Open Source Project 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 android.content.Context; 20 import android.graphics.Canvas; 21 import android.graphics.Paint; 22 import android.inputmethodservice.Keyboard; 23 import android.inputmethodservice.Keyboard.Key; 24 import android.os.Handler; 25 import android.os.Message; 26 import android.os.SystemClock; 27 import android.text.TextUtils; 28 import android.util.AttributeSet; 29 import android.view.MotionEvent; 30 31 import java.util.List; 32 33 public class LatinKeyboardView extends LatinKeyboardBaseView { 34 35 static final int KEYCODE_OPTIONS = -100; 36 static final int KEYCODE_OPTIONS_LONGPRESS = -101; 37 static final int KEYCODE_VOICE = -102; 38 static final int KEYCODE_F1 = -103; 39 static final int KEYCODE_NEXT_LANGUAGE = -104; 40 static final int KEYCODE_PREV_LANGUAGE = -105; 41 42 private Keyboard mPhoneKeyboard; 43 44 /** Whether we've started dropping move events because we found a big jump */ 45 private boolean mDroppingEvents; 46 /** 47 * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has 48 * occured 49 */ 50 private boolean mDisableDisambiguation; 51 /** The distance threshold at which we start treating the touch session as a multi-touch */ 52 private int mJumpThresholdSquare = Integer.MAX_VALUE; 53 /** The y coordinate of the last row */ 54 private int mLastRowY; 55 LatinKeyboardView(Context context, AttributeSet attrs)56 public LatinKeyboardView(Context context, AttributeSet attrs) { 57 this(context, attrs, 0); 58 } 59 LatinKeyboardView(Context context, AttributeSet attrs, int defStyle)60 public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) { 61 super(context, attrs, defStyle); 62 } 63 setPhoneKeyboard(Keyboard phoneKeyboard)64 public void setPhoneKeyboard(Keyboard phoneKeyboard) { 65 mPhoneKeyboard = phoneKeyboard; 66 } 67 68 @Override setPreviewEnabled(boolean previewEnabled)69 public void setPreviewEnabled(boolean previewEnabled) { 70 if (getKeyboard() == mPhoneKeyboard) { 71 // Phone keyboard never shows popup preview (except language switch). 72 super.setPreviewEnabled(false); 73 } else { 74 super.setPreviewEnabled(previewEnabled); 75 } 76 } 77 78 @Override setKeyboard(Keyboard newKeyboard)79 public void setKeyboard(Keyboard newKeyboard) { 80 final Keyboard oldKeyboard = getKeyboard(); 81 if (oldKeyboard instanceof LatinKeyboard) { 82 // Reset old keyboard state before switching to new keyboard. 83 ((LatinKeyboard)oldKeyboard).keyReleased(); 84 } 85 super.setKeyboard(newKeyboard); 86 // One-seventh of the keyboard width seems like a reasonable threshold 87 mJumpThresholdSquare = newKeyboard.getMinWidth() / 7; 88 mJumpThresholdSquare *= mJumpThresholdSquare; 89 // Assuming there are 4 rows, this is the coordinate of the last row 90 mLastRowY = (newKeyboard.getHeight() * 3) / 4; 91 setKeyboardLocal(newKeyboard); 92 } 93 94 @Override onLongPress(Key key)95 protected boolean onLongPress(Key key) { 96 int primaryCode = key.codes[0]; 97 if (primaryCode == KEYCODE_OPTIONS) { 98 return invokeOnKey(KEYCODE_OPTIONS_LONGPRESS); 99 } else if (primaryCode == '0' && getKeyboard() == mPhoneKeyboard) { 100 // Long pressing on 0 in phone number keypad gives you a '+'. 101 return invokeOnKey('+'); 102 } else { 103 return super.onLongPress(key); 104 } 105 } 106 invokeOnKey(int primaryCode)107 private boolean invokeOnKey(int primaryCode) { 108 getOnKeyboardActionListener().onKey(primaryCode, null, 109 LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE, 110 LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE); 111 return true; 112 } 113 114 @Override adjustCase(CharSequence label)115 protected CharSequence adjustCase(CharSequence label) { 116 Keyboard keyboard = getKeyboard(); 117 if (keyboard.isShifted() 118 && keyboard instanceof LatinKeyboard 119 && ((LatinKeyboard) keyboard).isAlphaKeyboard() 120 && !TextUtils.isEmpty(label) && label.length() < 3 121 && Character.isLowerCase(label.charAt(0))) { 122 return label.toString().toUpperCase(getKeyboardLocale()); 123 } 124 return label; 125 } 126 setShiftLocked(boolean shiftLocked)127 public boolean setShiftLocked(boolean shiftLocked) { 128 Keyboard keyboard = getKeyboard(); 129 if (keyboard instanceof LatinKeyboard) { 130 ((LatinKeyboard)keyboard).setShiftLocked(shiftLocked); 131 invalidateAllKeys(); 132 return true; 133 } 134 return false; 135 } 136 137 /** 138 * This function checks to see if we need to handle any sudden jumps in the pointer location 139 * that could be due to a multi-touch being treated as a move by the firmware or hardware. 140 * Once a sudden jump is detected, all subsequent move events are discarded 141 * until an UP is received.<P> 142 * When a sudden jump is detected, an UP event is simulated at the last position and when 143 * the sudden moves subside, a DOWN event is simulated for the second key. 144 * @param me the motion event 145 * @return true if the event was consumed, so that it doesn't continue to be handled by 146 * KeyboardView. 147 */ handleSuddenJump(MotionEvent me)148 private boolean handleSuddenJump(MotionEvent me) { 149 final int action = me.getAction(); 150 final int x = (int) me.getX(); 151 final int y = (int) me.getY(); 152 boolean result = false; 153 154 // Real multi-touch event? Stop looking for sudden jumps 155 if (me.getPointerCount() > 1) { 156 mDisableDisambiguation = true; 157 } 158 if (mDisableDisambiguation) { 159 // If UP, reset the multi-touch flag 160 if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false; 161 return false; 162 } 163 164 switch (action) { 165 case MotionEvent.ACTION_DOWN: 166 // Reset the "session" 167 mDroppingEvents = false; 168 mDisableDisambiguation = false; 169 break; 170 case MotionEvent.ACTION_MOVE: 171 // Is this a big jump? 172 final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y); 173 // Check the distance and also if the move is not entirely within the bottom row 174 // If it's only in the bottom row, it might be an intentional slide gesture 175 // for language switching 176 if (distanceSquare > mJumpThresholdSquare 177 && (mLastY < mLastRowY || y < mLastRowY)) { 178 // If we're not yet dropping events, start dropping and send an UP event 179 if (!mDroppingEvents) { 180 mDroppingEvents = true; 181 // Send an up event 182 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 183 MotionEvent.ACTION_UP, 184 mLastX, mLastY, me.getMetaState()); 185 super.onTouchEvent(translated); 186 translated.recycle(); 187 } 188 result = true; 189 } else if (mDroppingEvents) { 190 // If moves are small and we're already dropping events, continue dropping 191 result = true; 192 } 193 break; 194 case MotionEvent.ACTION_UP: 195 if (mDroppingEvents) { 196 // Send a down event first, as we dropped a bunch of sudden jumps and assume that 197 // the user is releasing the touch on the second key. 198 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 199 MotionEvent.ACTION_DOWN, 200 x, y, me.getMetaState()); 201 super.onTouchEvent(translated); 202 translated.recycle(); 203 mDroppingEvents = false; 204 // Let the up event get processed as well, result = false 205 } 206 break; 207 } 208 // Track the previous coordinate 209 mLastX = x; 210 mLastY = y; 211 return result; 212 } 213 214 @Override onTouchEvent(MotionEvent me)215 public boolean onTouchEvent(MotionEvent me) { 216 LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); 217 if (DEBUG_LINE) { 218 mLastX = (int) me.getX(); 219 mLastY = (int) me.getY(); 220 invalidate(); 221 } 222 223 // If there was a sudden jump, return without processing the actual motion event. 224 if (handleSuddenJump(me)) 225 return true; 226 227 // Reset any bounding box controls in the keyboard 228 if (me.getAction() == MotionEvent.ACTION_DOWN) { 229 keyboard.keyReleased(); 230 } 231 232 if (me.getAction() == MotionEvent.ACTION_UP) { 233 int languageDirection = keyboard.getLanguageChangeDirection(); 234 if (languageDirection != 0) { 235 getOnKeyboardActionListener().onKey( 236 languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE, 237 null, mLastX, mLastY); 238 me.setAction(MotionEvent.ACTION_CANCEL); 239 keyboard.keyReleased(); 240 return super.onTouchEvent(me); 241 } 242 } 243 244 return super.onTouchEvent(me); 245 } 246 247 /**************************** INSTRUMENTATION *******************************/ 248 249 static final boolean DEBUG_AUTO_PLAY = false; 250 static final boolean DEBUG_LINE = false; 251 private static final int MSG_TOUCH_DOWN = 1; 252 private static final int MSG_TOUCH_UP = 2; 253 254 Handler mHandler2; 255 256 private String mStringToPlay; 257 private int mStringIndex; 258 private boolean mDownDelivered; 259 private Key[] mAsciiKeys = new Key[256]; 260 private boolean mPlaying; 261 private int mLastX; 262 private int mLastY; 263 private Paint mPaint; 264 setKeyboardLocal(Keyboard k)265 private void setKeyboardLocal(Keyboard k) { 266 if (DEBUG_AUTO_PLAY) { 267 findKeys(); 268 if (mHandler2 == null) { 269 mHandler2 = new Handler() { 270 @Override 271 public void handleMessage(Message msg) { 272 removeMessages(MSG_TOUCH_DOWN); 273 removeMessages(MSG_TOUCH_UP); 274 if (mPlaying == false) return; 275 276 switch (msg.what) { 277 case MSG_TOUCH_DOWN: 278 if (mStringIndex >= mStringToPlay.length()) { 279 mPlaying = false; 280 return; 281 } 282 char c = mStringToPlay.charAt(mStringIndex); 283 while (c > 255 || mAsciiKeys[c] == null) { 284 mStringIndex++; 285 if (mStringIndex >= mStringToPlay.length()) { 286 mPlaying = false; 287 return; 288 } 289 c = mStringToPlay.charAt(mStringIndex); 290 } 291 int x = mAsciiKeys[c].x + 10; 292 int y = mAsciiKeys[c].y + 26; 293 MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(), 294 SystemClock.uptimeMillis(), 295 MotionEvent.ACTION_DOWN, x, y, 0); 296 LatinKeyboardView.this.dispatchTouchEvent(me); 297 me.recycle(); 298 sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else 299 // happens 300 mDownDelivered = true; 301 break; 302 case MSG_TOUCH_UP: 303 char cUp = mStringToPlay.charAt(mStringIndex); 304 int x2 = mAsciiKeys[cUp].x + 10; 305 int y2 = mAsciiKeys[cUp].y + 26; 306 mStringIndex++; 307 308 MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(), 309 SystemClock.uptimeMillis(), 310 MotionEvent.ACTION_UP, x2, y2, 0); 311 LatinKeyboardView.this.dispatchTouchEvent(me2); 312 me2.recycle(); 313 sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else 314 // happens 315 mDownDelivered = false; 316 break; 317 } 318 } 319 }; 320 321 } 322 } 323 } 324 findKeys()325 private void findKeys() { 326 List<Key> keys = getKeyboard().getKeys(); 327 // Get the keys on this keyboard 328 for (int i = 0; i < keys.size(); i++) { 329 int code = keys.get(i).codes[0]; 330 if (code >= 0 && code <= 255) { 331 mAsciiKeys[code] = keys.get(i); 332 } 333 } 334 } 335 startPlaying(String s)336 public void startPlaying(String s) { 337 if (DEBUG_AUTO_PLAY) { 338 if (s == null) return; 339 mStringToPlay = s.toLowerCase(); 340 mPlaying = true; 341 mDownDelivered = false; 342 mStringIndex = 0; 343 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); 344 } 345 } 346 347 @Override draw(Canvas c)348 public void draw(Canvas c) { 349 LatinIMEUtil.GCUtils.getInstance().reset(); 350 boolean tryGC = true; 351 for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { 352 try { 353 super.draw(c); 354 tryGC = false; 355 } catch (OutOfMemoryError e) { 356 tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e); 357 } 358 } 359 if (DEBUG_AUTO_PLAY) { 360 if (mPlaying) { 361 mHandler2.removeMessages(MSG_TOUCH_DOWN); 362 mHandler2.removeMessages(MSG_TOUCH_UP); 363 if (mDownDelivered) { 364 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); 365 } else { 366 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); 367 } 368 } 369 } 370 if (DEBUG_LINE) { 371 if (mPaint == null) { 372 mPaint = new Paint(); 373 mPaint.setColor(0x80FFFFFF); 374 mPaint.setAntiAlias(false); 375 } 376 c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint); 377 c.drawLine(0, mLastY, getWidth(), mLastY, mPaint); 378 } 379 } 380 } 381