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