Home | History | Annotate | Download | only in latin
      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