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