• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.inputmethodservice;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.PorterDuff;
25 import android.graphics.Rect;
26 import android.graphics.Typeface;
27 import android.graphics.Paint.Align;
28 import android.graphics.Region.Op;
29 import android.graphics.drawable.Drawable;
30 import android.inputmethodservice.Keyboard.Key;
31 import android.media.AudioManager;
32 import android.os.Handler;
33 import android.os.Message;
34 import android.util.AttributeSet;
35 import android.util.TypedValue;
36 import android.view.GestureDetector;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewConfiguration;
42 import android.view.ViewGroup.LayoutParams;
43 import android.view.accessibility.AccessibilityEvent;
44 import android.view.accessibility.AccessibilityManager;
45 import android.widget.PopupWindow;
46 import android.widget.TextView;
47 
48 import com.android.internal.R;
49 
50 import java.util.Arrays;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 
55 /**
56  * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and
57  * detecting key presses and touch movements.
58  *
59  * @attr ref android.R.styleable#KeyboardView_keyBackground
60  * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
61  * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
62  * @attr ref android.R.styleable#KeyboardView_labelTextSize
63  * @attr ref android.R.styleable#KeyboardView_keyTextSize
64  * @attr ref android.R.styleable#KeyboardView_keyTextColor
65  * @attr ref android.R.styleable#KeyboardView_verticalCorrection
66  * @attr ref android.R.styleable#KeyboardView_popupLayout
67  */
68 public class KeyboardView extends View implements View.OnClickListener {
69 
70     /**
71      * Listener for virtual keyboard events.
72      */
73     public interface OnKeyboardActionListener {
74 
75         /**
76          * Called when the user presses a key. This is sent before the {@link #onKey} is called.
77          * For keys that repeat, this is only called once.
78          * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
79          * key, the value will be zero.
80          */
onPress(int primaryCode)81         void onPress(int primaryCode);
82 
83         /**
84          * Called when the user releases a key. This is sent after the {@link #onKey} is called.
85          * For keys that repeat, this is only called once.
86          * @param primaryCode the code of the key that was released
87          */
onRelease(int primaryCode)88         void onRelease(int primaryCode);
89 
90         /**
91          * Send a key press to the listener.
92          * @param primaryCode this is the key that was pressed
93          * @param keyCodes the codes for all the possible alternative keys
94          * with the primary code being the first. If the primary key code is
95          * a single character such as an alphabet or number or symbol, the alternatives
96          * will include other characters that may be on the same key or adjacent keys.
97          * These codes are useful to correct for accidental presses of a key adjacent to
98          * the intended key.
99          */
onKey(int primaryCode, int[] keyCodes)100         void onKey(int primaryCode, int[] keyCodes);
101 
102         /**
103          * Sends a sequence of characters to the listener.
104          * @param text the sequence of characters to be displayed.
105          */
onText(CharSequence text)106         void onText(CharSequence text);
107 
108         /**
109          * Called when the user quickly moves the finger from right to left.
110          */
swipeLeft()111         void swipeLeft();
112 
113         /**
114          * Called when the user quickly moves the finger from left to right.
115          */
swipeRight()116         void swipeRight();
117 
118         /**
119          * Called when the user quickly moves the finger from up to down.
120          */
swipeDown()121         void swipeDown();
122 
123         /**
124          * Called when the user quickly moves the finger from down to up.
125          */
swipeUp()126         void swipeUp();
127     }
128 
129     private static final boolean DEBUG = false;
130     private static final int NOT_A_KEY = -1;
131     private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
132     private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
133 
134     private Keyboard mKeyboard;
135     private int mCurrentKeyIndex = NOT_A_KEY;
136     private int mLabelTextSize;
137     private int mKeyTextSize;
138     private int mKeyTextColor;
139     private float mShadowRadius;
140     private int mShadowColor;
141     private float mBackgroundDimAmount;
142 
143     private TextView mPreviewText;
144     private PopupWindow mPreviewPopup;
145     private int mPreviewTextSizeLarge;
146     private int mPreviewOffset;
147     private int mPreviewHeight;
148     // Working variable
149     private final int[] mCoordinates = new int[2];
150 
151     private PopupWindow mPopupKeyboard;
152     private View mMiniKeyboardContainer;
153     private KeyboardView mMiniKeyboard;
154     private boolean mMiniKeyboardOnScreen;
155     private View mPopupParent;
156     private int mMiniKeyboardOffsetX;
157     private int mMiniKeyboardOffsetY;
158     private Map<Key,View> mMiniKeyboardCache;
159     private Key[] mKeys;
160 
161     /** Listener for {@link OnKeyboardActionListener}. */
162     private OnKeyboardActionListener mKeyboardActionListener;
163 
164     private static final int MSG_SHOW_PREVIEW = 1;
165     private static final int MSG_REMOVE_PREVIEW = 2;
166     private static final int MSG_REPEAT = 3;
167     private static final int MSG_LONGPRESS = 4;
168 
169     private static final int DELAY_BEFORE_PREVIEW = 0;
170     private static final int DELAY_AFTER_PREVIEW = 70;
171     private static final int DEBOUNCE_TIME = 70;
172 
173     private int mVerticalCorrection;
174     private int mProximityThreshold;
175 
176     private boolean mPreviewCentered = false;
177     private boolean mShowPreview = true;
178     private boolean mShowTouchPoints = true;
179     private int mPopupPreviewX;
180     private int mPopupPreviewY;
181 
182     private int mLastX;
183     private int mLastY;
184     private int mStartX;
185     private int mStartY;
186 
187     private boolean mProximityCorrectOn;
188 
189     private Paint mPaint;
190     private Rect mPadding;
191 
192     private long mDownTime;
193     private long mLastMoveTime;
194     private int mLastKey;
195     private int mLastCodeX;
196     private int mLastCodeY;
197     private int mCurrentKey = NOT_A_KEY;
198     private int mDownKey = NOT_A_KEY;
199     private long mLastKeyTime;
200     private long mCurrentKeyTime;
201     private int[] mKeyIndices = new int[12];
202     private GestureDetector mGestureDetector;
203     private int mPopupX;
204     private int mPopupY;
205     private int mRepeatKeyIndex = NOT_A_KEY;
206     private int mPopupLayout;
207     private boolean mAbortKey;
208     private Key mInvalidatedKey;
209     private Rect mClipRegion = new Rect(0, 0, 0, 0);
210     private boolean mPossiblePoly;
211     private SwipeTracker mSwipeTracker = new SwipeTracker();
212     private int mSwipeThreshold;
213     private boolean mDisambiguateSwipe;
214 
215     // Variables for dealing with multiple pointers
216     private int mOldPointerCount = 1;
217     private float mOldPointerX;
218     private float mOldPointerY;
219 
220     private Drawable mKeyBackground;
221 
222     private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
223     private static final int REPEAT_START_DELAY = 400;
224     private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
225 
226     private static int MAX_NEARBY_KEYS = 12;
227     private int[] mDistances = new int[MAX_NEARBY_KEYS];
228 
229     // For multi-tap
230     private int mLastSentIndex;
231     private int mTapCount;
232     private long mLastTapTime;
233     private boolean mInMultiTap;
234     private static final int MULTITAP_INTERVAL = 800; // milliseconds
235     private StringBuilder mPreviewLabel = new StringBuilder(1);
236 
237     /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
238     private boolean mDrawPending;
239     /** The dirty region in the keyboard bitmap */
240     private Rect mDirtyRect = new Rect();
241     /** The keyboard bitmap for faster updates */
242     private Bitmap mBuffer;
243     /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
244     private boolean mKeyboardChanged;
245     /** The canvas for the above mutable keyboard bitmap */
246     private Canvas mCanvas;
247     /** The accessibility manager for accessibility support */
248     private AccessibilityManager mAccessibilityManager;
249     /** The audio manager for accessibility support */
250     private AudioManager mAudioManager;
251     /** Whether the requirement of a headset to hear passwords if accessibility is enabled is announced. */
252     private boolean mHeadsetRequiredToHearPasswordsAnnounced;
253 
254     Handler mHandler = new Handler() {
255         @Override
256         public void handleMessage(Message msg) {
257             switch (msg.what) {
258                 case MSG_SHOW_PREVIEW:
259                     showKey(msg.arg1);
260                     break;
261                 case MSG_REMOVE_PREVIEW:
262                     mPreviewText.setVisibility(INVISIBLE);
263                     break;
264                 case MSG_REPEAT:
265                     if (repeatKey()) {
266                         Message repeat = Message.obtain(this, MSG_REPEAT);
267                         sendMessageDelayed(repeat, REPEAT_INTERVAL);
268                     }
269                     break;
270                 case MSG_LONGPRESS:
271                     openPopupIfRequired((MotionEvent) msg.obj);
272                     break;
273             }
274         }
275     };
276 
KeyboardView(Context context, AttributeSet attrs)277     public KeyboardView(Context context, AttributeSet attrs) {
278         this(context, attrs, com.android.internal.R.attr.keyboardViewStyle);
279     }
280 
KeyboardView(Context context, AttributeSet attrs, int defStyle)281     public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
282         super(context, attrs, defStyle);
283 
284         TypedArray a =
285             context.obtainStyledAttributes(
286                 attrs, android.R.styleable.KeyboardView, defStyle, 0);
287 
288         LayoutInflater inflate =
289                 (LayoutInflater) context
290                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
291 
292         int previewLayout = 0;
293         int keyTextSize = 0;
294 
295         int n = a.getIndexCount();
296 
297         for (int i = 0; i < n; i++) {
298             int attr = a.getIndex(i);
299 
300             switch (attr) {
301             case com.android.internal.R.styleable.KeyboardView_keyBackground:
302                 mKeyBackground = a.getDrawable(attr);
303                 break;
304             case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
305                 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
306                 break;
307             case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
308                 previewLayout = a.getResourceId(attr, 0);
309                 break;
310             case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
311                 mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
312                 break;
313             case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
314                 mPreviewHeight = a.getDimensionPixelSize(attr, 80);
315                 break;
316             case com.android.internal.R.styleable.KeyboardView_keyTextSize:
317                 mKeyTextSize = a.getDimensionPixelSize(attr, 18);
318                 break;
319             case com.android.internal.R.styleable.KeyboardView_keyTextColor:
320                 mKeyTextColor = a.getColor(attr, 0xFF000000);
321                 break;
322             case com.android.internal.R.styleable.KeyboardView_labelTextSize:
323                 mLabelTextSize = a.getDimensionPixelSize(attr, 14);
324                 break;
325             case com.android.internal.R.styleable.KeyboardView_popupLayout:
326                 mPopupLayout = a.getResourceId(attr, 0);
327                 break;
328             case com.android.internal.R.styleable.KeyboardView_shadowColor:
329                 mShadowColor = a.getColor(attr, 0);
330                 break;
331             case com.android.internal.R.styleable.KeyboardView_shadowRadius:
332                 mShadowRadius = a.getFloat(attr, 0f);
333                 break;
334             }
335         }
336 
337         a = mContext.obtainStyledAttributes(
338                 com.android.internal.R.styleable.Theme);
339         mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
340 
341         mPreviewPopup = new PopupWindow(context);
342         if (previewLayout != 0) {
343             mPreviewText = (TextView) inflate.inflate(previewLayout, null);
344             mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
345             mPreviewPopup.setContentView(mPreviewText);
346             mPreviewPopup.setBackgroundDrawable(null);
347         } else {
348             mShowPreview = false;
349         }
350 
351         mPreviewPopup.setTouchable(false);
352 
353         mPopupKeyboard = new PopupWindow(context);
354         mPopupKeyboard.setBackgroundDrawable(null);
355         //mPopupKeyboard.setClippingEnabled(false);
356 
357         mPopupParent = this;
358         //mPredicting = true;
359 
360         mPaint = new Paint();
361         mPaint.setAntiAlias(true);
362         mPaint.setTextSize(keyTextSize);
363         mPaint.setTextAlign(Align.CENTER);
364         mPaint.setAlpha(255);
365 
366         mPadding = new Rect(0, 0, 0, 0);
367         mMiniKeyboardCache = new HashMap<Key,View>();
368         mKeyBackground.getPadding(mPadding);
369 
370         mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
371         mDisambiguateSwipe = getResources().getBoolean(
372                 com.android.internal.R.bool.config_swipeDisambiguation);
373 
374         mAccessibilityManager = AccessibilityManager.getInstance(context);
375         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
376 
377         resetMultiTap();
378         initGestureDetector();
379     }
380 
381 
initGestureDetector()382     private void initGestureDetector() {
383         mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
384             @Override
385             public boolean onFling(MotionEvent me1, MotionEvent me2,
386                     float velocityX, float velocityY) {
387                 if (mPossiblePoly) return false;
388                 final float absX = Math.abs(velocityX);
389                 final float absY = Math.abs(velocityY);
390                 float deltaX = me2.getX() - me1.getX();
391                 float deltaY = me2.getY() - me1.getY();
392                 int travelX = getWidth() / 2; // Half the keyboard width
393                 int travelY = getHeight() / 2; // Half the keyboard height
394                 mSwipeTracker.computeCurrentVelocity(1000);
395                 final float endingVelocityX = mSwipeTracker.getXVelocity();
396                 final float endingVelocityY = mSwipeTracker.getYVelocity();
397                 boolean sendDownKey = false;
398                 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
399                     if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) {
400                         sendDownKey = true;
401                     } else {
402                         swipeRight();
403                         return true;
404                     }
405                 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
406                     if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) {
407                         sendDownKey = true;
408                     } else {
409                         swipeLeft();
410                         return true;
411                     }
412                 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
413                     if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) {
414                         sendDownKey = true;
415                     } else {
416                         swipeUp();
417                         return true;
418                     }
419                 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
420                     if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) {
421                         sendDownKey = true;
422                     } else {
423                         swipeDown();
424                         return true;
425                     }
426                 }
427 
428                 if (sendDownKey) {
429                     detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime());
430                 }
431                 return false;
432             }
433         });
434 
435         mGestureDetector.setIsLongpressEnabled(false);
436     }
437 
setOnKeyboardActionListener(OnKeyboardActionListener listener)438     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
439         mKeyboardActionListener = listener;
440     }
441 
442     /**
443      * Returns the {@link OnKeyboardActionListener} object.
444      * @return the listener attached to this keyboard
445      */
getOnKeyboardActionListener()446     protected OnKeyboardActionListener getOnKeyboardActionListener() {
447         return mKeyboardActionListener;
448     }
449 
450     /**
451      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
452      * view will re-layout itself to accommodate the keyboard.
453      * @see Keyboard
454      * @see #getKeyboard()
455      * @param keyboard the keyboard to display in this view
456      */
setKeyboard(Keyboard keyboard)457     public void setKeyboard(Keyboard keyboard) {
458         if (mKeyboard != null) {
459             showPreview(NOT_A_KEY);
460         }
461         // Remove any pending messages
462         removeMessages();
463         mKeyboard = keyboard;
464         List<Key> keys = mKeyboard.getKeys();
465         mKeys = keys.toArray(new Key[keys.size()]);
466         requestLayout();
467         // Hint to reallocate the buffer if the size changed
468         mKeyboardChanged = true;
469         invalidateAllKeys();
470         computeProximityThreshold(keyboard);
471         mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views
472         // Switching to a different keyboard should abort any pending keys so that the key up
473         // doesn't get delivered to the old or new keyboard
474         mAbortKey = true; // Until the next ACTION_DOWN
475     }
476 
477     /**
478      * Returns the current keyboard being displayed by this view.
479      * @return the currently attached keyboard
480      * @see #setKeyboard(Keyboard)
481      */
getKeyboard()482     public Keyboard getKeyboard() {
483         return mKeyboard;
484     }
485 
486     /**
487      * Sets the state of the shift key of the keyboard, if any.
488      * @param shifted whether or not to enable the state of the shift key
489      * @return true if the shift key state changed, false if there was no change
490      * @see KeyboardView#isShifted()
491      */
setShifted(boolean shifted)492     public boolean setShifted(boolean shifted) {
493         if (mKeyboard != null) {
494             if (mKeyboard.setShifted(shifted)) {
495                 // The whole keyboard probably needs to be redrawn
496                 invalidateAllKeys();
497                 return true;
498             }
499         }
500         return false;
501     }
502 
503     /**
504      * Returns the state of the shift key of the keyboard, if any.
505      * @return true if the shift is in a pressed state, false otherwise. If there is
506      * no shift key on the keyboard or there is no keyboard attached, it returns false.
507      * @see KeyboardView#setShifted(boolean)
508      */
isShifted()509     public boolean isShifted() {
510         if (mKeyboard != null) {
511             return mKeyboard.isShifted();
512         }
513         return false;
514     }
515 
516     /**
517      * Enables or disables the key feedback popup. This is a popup that shows a magnified
518      * version of the depressed key. By default the preview is enabled.
519      * @param previewEnabled whether or not to enable the key feedback popup
520      * @see #isPreviewEnabled()
521      */
setPreviewEnabled(boolean previewEnabled)522     public void setPreviewEnabled(boolean previewEnabled) {
523         mShowPreview = previewEnabled;
524     }
525 
526     /**
527      * Returns the enabled state of the key feedback popup.
528      * @return whether or not the key feedback popup is enabled
529      * @see #setPreviewEnabled(boolean)
530      */
isPreviewEnabled()531     public boolean isPreviewEnabled() {
532         return mShowPreview;
533     }
534 
setVerticalCorrection(int verticalOffset)535     public void setVerticalCorrection(int verticalOffset) {
536 
537     }
setPopupParent(View v)538     public void setPopupParent(View v) {
539         mPopupParent = v;
540     }
541 
setPopupOffset(int x, int y)542     public void setPopupOffset(int x, int y) {
543         mMiniKeyboardOffsetX = x;
544         mMiniKeyboardOffsetY = y;
545         if (mPreviewPopup.isShowing()) {
546             mPreviewPopup.dismiss();
547         }
548     }
549 
550     /**
551      * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
552      * codes for adjacent keys.  When disabled, only the primary key code will be
553      * reported.
554      * @param enabled whether or not the proximity correction is enabled
555      */
setProximityCorrectionEnabled(boolean enabled)556     public void setProximityCorrectionEnabled(boolean enabled) {
557         mProximityCorrectOn = enabled;
558     }
559 
560     /**
561      * Returns true if proximity correction is enabled.
562      */
isProximityCorrectionEnabled()563     public boolean isProximityCorrectionEnabled() {
564         return mProximityCorrectOn;
565     }
566 
567     /**
568      * Popup keyboard close button clicked.
569      * @hide
570      */
onClick(View v)571     public void onClick(View v) {
572         dismissPopupKeyboard();
573     }
574 
adjustCase(CharSequence label)575     private CharSequence adjustCase(CharSequence label) {
576         if (mKeyboard.isShifted() && label != null && label.length() < 3
577                 && Character.isLowerCase(label.charAt(0))) {
578             label = label.toString().toUpperCase();
579         }
580         return label;
581     }
582 
583     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)584     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
585         // Round up a little
586         if (mKeyboard == null) {
587             setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom);
588         } else {
589             int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight;
590             if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
591                 width = MeasureSpec.getSize(widthMeasureSpec);
592             }
593             setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom);
594         }
595     }
596 
597     /**
598      * Compute the average distance between adjacent keys (horizontally and vertically)
599      * and square it to get the proximity threshold. We use a square here and in computing
600      * the touch distance from a key's center to avoid taking a square root.
601      * @param keyboard
602      */
computeProximityThreshold(Keyboard keyboard)603     private void computeProximityThreshold(Keyboard keyboard) {
604         if (keyboard == null) return;
605         final Key[] keys = mKeys;
606         if (keys == null) return;
607         int length = keys.length;
608         int dimensionSum = 0;
609         for (int i = 0; i < length; i++) {
610             Key key = keys[i];
611             dimensionSum += Math.min(key.width, key.height) + key.gap;
612         }
613         if (dimensionSum < 0 || length == 0) return;
614         mProximityThreshold = (int) (dimensionSum * 1.4f / length);
615         mProximityThreshold *= mProximityThreshold; // Square it
616     }
617 
618     @Override
onSizeChanged(int w, int h, int oldw, int oldh)619     public void onSizeChanged(int w, int h, int oldw, int oldh) {
620         super.onSizeChanged(w, h, oldw, oldh);
621         if (mKeyboard != null) {
622             mKeyboard.resize(w, h);
623         }
624         // Release the buffer, if any and it will be reallocated on the next draw
625         mBuffer = null;
626     }
627 
628     @Override
onDraw(Canvas canvas)629     public void onDraw(Canvas canvas) {
630         super.onDraw(canvas);
631         if (mDrawPending || mBuffer == null || mKeyboardChanged) {
632             onBufferDraw();
633         }
634         canvas.drawBitmap(mBuffer, 0, 0, null);
635     }
636 
onBufferDraw()637     private void onBufferDraw() {
638         if (mBuffer == null || mKeyboardChanged) {
639             if (mBuffer == null || mKeyboardChanged &&
640                     (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
641                 // Make sure our bitmap is at least 1x1
642                 final int width = Math.max(1, getWidth());
643                 final int height = Math.max(1, getHeight());
644                 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
645                 mCanvas = new Canvas(mBuffer);
646             }
647             invalidateAllKeys();
648             mKeyboardChanged = false;
649         }
650         final Canvas canvas = mCanvas;
651         canvas.clipRect(mDirtyRect, Op.REPLACE);
652 
653         if (mKeyboard == null) return;
654 
655         final Paint paint = mPaint;
656         final Drawable keyBackground = mKeyBackground;
657         final Rect clipRegion = mClipRegion;
658         final Rect padding = mPadding;
659         final int kbdPaddingLeft = mPaddingLeft;
660         final int kbdPaddingTop = mPaddingTop;
661         final Key[] keys = mKeys;
662         final Key invalidKey = mInvalidatedKey;
663 
664         paint.setColor(mKeyTextColor);
665         boolean drawSingleKey = false;
666         if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
667           // Is clipRegion completely contained within the invalidated key?
668           if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
669                   invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
670                   invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
671                   invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
672               drawSingleKey = true;
673           }
674         }
675         canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
676         final int keyCount = keys.length;
677         for (int i = 0; i < keyCount; i++) {
678             final Key key = keys[i];
679             if (drawSingleKey && invalidKey != key) {
680                 continue;
681             }
682             int[] drawableState = key.getCurrentDrawableState();
683             keyBackground.setState(drawableState);
684 
685             // Switch the character to uppercase if shift is pressed
686             String label = key.label == null? null : adjustCase(key.label).toString();
687 
688             final Rect bounds = keyBackground.getBounds();
689             if (key.width != bounds.right ||
690                     key.height != bounds.bottom) {
691                 keyBackground.setBounds(0, 0, key.width, key.height);
692             }
693             canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
694             keyBackground.draw(canvas);
695 
696             if (label != null) {
697                 // For characters, use large font. For labels like "Done", use small font.
698                 if (label.length() > 1 && key.codes.length < 2) {
699                     paint.setTextSize(mLabelTextSize);
700                     paint.setTypeface(Typeface.DEFAULT_BOLD);
701                 } else {
702                     paint.setTextSize(mKeyTextSize);
703                     paint.setTypeface(Typeface.DEFAULT);
704                 }
705                 // Draw a drop shadow for the text
706                 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
707                 // Draw the text
708                 canvas.drawText(label,
709                     (key.width - padding.left - padding.right) / 2
710                             + padding.left,
711                     (key.height - padding.top - padding.bottom) / 2
712                             + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
713                     paint);
714                 // Turn off drop shadow
715                 paint.setShadowLayer(0, 0, 0, 0);
716             } else if (key.icon != null) {
717                 final int drawableX = (key.width - padding.left - padding.right
718                                 - key.icon.getIntrinsicWidth()) / 2 + padding.left;
719                 final int drawableY = (key.height - padding.top - padding.bottom
720                         - key.icon.getIntrinsicHeight()) / 2 + padding.top;
721                 canvas.translate(drawableX, drawableY);
722                 key.icon.setBounds(0, 0,
723                         key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
724                 key.icon.draw(canvas);
725                 canvas.translate(-drawableX, -drawableY);
726             }
727             canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
728         }
729         mInvalidatedKey = null;
730         // Overlay a dark rectangle to dim the keyboard
731         if (mMiniKeyboardOnScreen) {
732             paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
733             canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
734         }
735 
736         if (DEBUG && mShowTouchPoints) {
737             paint.setAlpha(128);
738             paint.setColor(0xFFFF0000);
739             canvas.drawCircle(mStartX, mStartY, 3, paint);
740             canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
741             paint.setColor(0xFF0000FF);
742             canvas.drawCircle(mLastX, mLastY, 3, paint);
743             paint.setColor(0xFF00FF00);
744             canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
745         }
746 
747         mDrawPending = false;
748         mDirtyRect.setEmpty();
749     }
750 
getKeyIndices(int x, int y, int[] allKeys)751     private int getKeyIndices(int x, int y, int[] allKeys) {
752         final Key[] keys = mKeys;
753         int primaryIndex = NOT_A_KEY;
754         int closestKey = NOT_A_KEY;
755         int closestKeyDist = mProximityThreshold + 1;
756         java.util.Arrays.fill(mDistances, Integer.MAX_VALUE);
757         int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
758         final int keyCount = nearestKeyIndices.length;
759         for (int i = 0; i < keyCount; i++) {
760             final Key key = keys[nearestKeyIndices[i]];
761             int dist = 0;
762             boolean isInside = key.isInside(x,y);
763             if (isInside) {
764                 primaryIndex = nearestKeyIndices[i];
765             }
766 
767             if (((mProximityCorrectOn
768                     && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
769                     || isInside)
770                     && key.codes[0] > 32) {
771                 // Find insertion point
772                 final int nCodes = key.codes.length;
773                 if (dist < closestKeyDist) {
774                     closestKeyDist = dist;
775                     closestKey = nearestKeyIndices[i];
776                 }
777 
778                 if (allKeys == null) continue;
779 
780                 for (int j = 0; j < mDistances.length; j++) {
781                     if (mDistances[j] > dist) {
782                         // Make space for nCodes codes
783                         System.arraycopy(mDistances, j, mDistances, j + nCodes,
784                                 mDistances.length - j - nCodes);
785                         System.arraycopy(allKeys, j, allKeys, j + nCodes,
786                                 allKeys.length - j - nCodes);
787                         for (int c = 0; c < nCodes; c++) {
788                             allKeys[j + c] = key.codes[c];
789                             mDistances[j + c] = dist;
790                         }
791                         break;
792                     }
793                 }
794             }
795         }
796         if (primaryIndex == NOT_A_KEY) {
797             primaryIndex = closestKey;
798         }
799         return primaryIndex;
800     }
801 
detectAndSendKey(int index, int x, int y, long eventTime)802     private void detectAndSendKey(int index, int x, int y, long eventTime) {
803         if (index != NOT_A_KEY && index < mKeys.length) {
804             final Key key = mKeys[index];
805             if (key.text != null) {
806                 mKeyboardActionListener.onText(key.text);
807                 mKeyboardActionListener.onRelease(NOT_A_KEY);
808             } else {
809                 int code = key.codes[0];
810                 //TextEntryState.keyPressedAt(key, x, y);
811                 int[] codes = new int[MAX_NEARBY_KEYS];
812                 Arrays.fill(codes, NOT_A_KEY);
813                 getKeyIndices(x, y, codes);
814                 // Multi-tap
815                 if (mInMultiTap) {
816                     if (mTapCount != -1) {
817                         mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
818                     } else {
819                         mTapCount = 0;
820                     }
821                     code = key.codes[mTapCount];
822                 }
823                 mKeyboardActionListener.onKey(code, codes);
824                 mKeyboardActionListener.onRelease(code);
825             }
826             mLastSentIndex = index;
827             mLastTapTime = eventTime;
828         }
829     }
830 
831     /**
832      * Handle multi-tap keys by producing the key label for the current multi-tap state.
833      */
getPreviewText(Key key)834     private CharSequence getPreviewText(Key key) {
835         if (mInMultiTap) {
836             // Multi-tap
837             mPreviewLabel.setLength(0);
838             mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
839             return adjustCase(mPreviewLabel);
840         } else {
841             return adjustCase(key.label);
842         }
843     }
844 
showPreview(int keyIndex)845     private void showPreview(int keyIndex) {
846         int oldKeyIndex = mCurrentKeyIndex;
847         final PopupWindow previewPopup = mPreviewPopup;
848 
849         mCurrentKeyIndex = keyIndex;
850         // Release the old key and press the new key
851         final Key[] keys = mKeys;
852         if (oldKeyIndex != mCurrentKeyIndex) {
853             if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
854                 Key oldKey = keys[oldKeyIndex];
855                 oldKey.onReleased(mCurrentKeyIndex == NOT_A_KEY);
856                 invalidateKey(oldKeyIndex);
857                 sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT,
858                         oldKey.codes[0]);
859             }
860             if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
861                 Key newKey = keys[mCurrentKeyIndex];
862                 newKey.onPressed();
863                 invalidateKey(mCurrentKeyIndex);
864                 sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
865                         newKey.codes[0]);
866             }
867         }
868         // If key changed and preview is on ...
869         if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
870             mHandler.removeMessages(MSG_SHOW_PREVIEW);
871             if (previewPopup.isShowing()) {
872                 if (keyIndex == NOT_A_KEY) {
873                     mHandler.sendMessageDelayed(mHandler
874                             .obtainMessage(MSG_REMOVE_PREVIEW),
875                             DELAY_AFTER_PREVIEW);
876                 }
877             }
878             if (keyIndex != NOT_A_KEY) {
879                 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
880                     // Show right away, if it's already visible and finger is moving around
881                     showKey(keyIndex);
882                 } else {
883                     mHandler.sendMessageDelayed(
884                             mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
885                             DELAY_BEFORE_PREVIEW);
886                 }
887             }
888         }
889     }
890 
showKey(final int keyIndex)891     private void showKey(final int keyIndex) {
892         final PopupWindow previewPopup = mPreviewPopup;
893         final Key[] keys = mKeys;
894         if (keyIndex < 0 || keyIndex >= mKeys.length) return;
895         Key key = keys[keyIndex];
896         if (key.icon != null) {
897             mPreviewText.setCompoundDrawables(null, null, null,
898                     key.iconPreview != null ? key.iconPreview : key.icon);
899             mPreviewText.setText(null);
900         } else {
901             mPreviewText.setCompoundDrawables(null, null, null, null);
902             mPreviewText.setText(getPreviewText(key));
903             if (key.label.length() > 1 && key.codes.length < 2) {
904                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
905                 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
906             } else {
907                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
908                 mPreviewText.setTypeface(Typeface.DEFAULT);
909             }
910         }
911         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
912                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
913         int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
914                 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
915         final int popupHeight = mPreviewHeight;
916         LayoutParams lp = mPreviewText.getLayoutParams();
917         if (lp != null) {
918             lp.width = popupWidth;
919             lp.height = popupHeight;
920         }
921         if (!mPreviewCentered) {
922             mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;
923             mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
924         } else {
925             // TODO: Fix this if centering is brought back
926             mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
927             mPopupPreviewY = - mPreviewText.getMeasuredHeight();
928         }
929         mHandler.removeMessages(MSG_REMOVE_PREVIEW);
930         getLocationInWindow(mCoordinates);
931         mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
932         mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero
933 
934         // Set the preview background state
935         mPreviewText.getBackground().setState(
936                 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
937         mPopupPreviewX += mCoordinates[0];
938         mPopupPreviewY += mCoordinates[1];
939 
940         // If the popup cannot be shown above the key, put it on the side
941         getLocationOnScreen(mCoordinates);
942         if (mPopupPreviewY + mCoordinates[1] < 0) {
943             // If the key you're pressing is on the left side of the keyboard, show the popup on
944             // the right, offset by enough to see at least one key to the left/right.
945             if (key.x + key.width <= getWidth() / 2) {
946                 mPopupPreviewX += (int) (key.width * 2.5);
947             } else {
948                 mPopupPreviewX -= (int) (key.width * 2.5);
949             }
950             mPopupPreviewY += popupHeight;
951         }
952 
953         if (previewPopup.isShowing()) {
954             previewPopup.update(mPopupPreviewX, mPopupPreviewY,
955                     popupWidth, popupHeight);
956         } else {
957             previewPopup.setWidth(popupWidth);
958             previewPopup.setHeight(popupHeight);
959             previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
960                     mPopupPreviewX, mPopupPreviewY);
961         }
962         mPreviewText.setVisibility(VISIBLE);
963     }
964 
sendAccessibilityEventForUnicodeCharacter(int eventType, int code)965     private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) {
966         if (mAccessibilityManager.isEnabled()) {
967             AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
968             onInitializeAccessibilityEvent(event);
969             String text = null;
970             // Add text only if headset is used to avoid leaking passwords.
971             if (mAudioManager.isBluetoothA2dpOn() || mAudioManager.isWiredHeadsetOn()) {
972                 switch (code) {
973                     case Keyboard.KEYCODE_ALT:
974                         text = mContext.getString(R.string.keyboardview_keycode_alt);
975                         break;
976                     case Keyboard.KEYCODE_CANCEL:
977                         text = mContext.getString(R.string.keyboardview_keycode_cancel);
978                         break;
979                     case Keyboard.KEYCODE_DELETE:
980                         text = mContext.getString(R.string.keyboardview_keycode_delete);
981                         break;
982                     case Keyboard.KEYCODE_DONE:
983                         text = mContext.getString(R.string.keyboardview_keycode_done);
984                         break;
985                     case Keyboard.KEYCODE_MODE_CHANGE:
986                         text = mContext.getString(R.string.keyboardview_keycode_mode_change);
987                         break;
988                     case Keyboard.KEYCODE_SHIFT:
989                         text = mContext.getString(R.string.keyboardview_keycode_shift);
990                         break;
991                     case '\n':
992                         text = mContext.getString(R.string.keyboardview_keycode_enter);
993                         break;
994                     default:
995                         text = String.valueOf((char) code);
996                 }
997             } else if (!mHeadsetRequiredToHearPasswordsAnnounced) {
998                 // We want the waring for required head set to be send with both the
999                 // hover enter and hover exit event, so set the flag after the exit.
1000                 if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) {
1001                     mHeadsetRequiredToHearPasswordsAnnounced = true;
1002                 }
1003                 text = mContext.getString(R.string.keyboard_headset_required_to_hear_password);
1004             } else {
1005                 text = mContext.getString(R.string.keyboard_password_character_no_headset);
1006             }
1007             event.getText().add(text);
1008             mAccessibilityManager.sendAccessibilityEvent(event);
1009         }
1010     }
1011 
1012     /**
1013      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1014      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1015      * draws the cached buffer.
1016      * @see #invalidateKey(int)
1017      */
invalidateAllKeys()1018     public void invalidateAllKeys() {
1019         mDirtyRect.union(0, 0, getWidth(), getHeight());
1020         mDrawPending = true;
1021         invalidate();
1022     }
1023 
1024     /**
1025      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1026      * one key is changing it's content. Any changes that affect the position or size of the key
1027      * may not be honored.
1028      * @param keyIndex the index of the key in the attached {@link Keyboard}.
1029      * @see #invalidateAllKeys
1030      */
invalidateKey(int keyIndex)1031     public void invalidateKey(int keyIndex) {
1032         if (mKeys == null) return;
1033         if (keyIndex < 0 || keyIndex >= mKeys.length) {
1034             return;
1035         }
1036         final Key key = mKeys[keyIndex];
1037         mInvalidatedKey = key;
1038         mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop,
1039                 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
1040         onBufferDraw();
1041         invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
1042                 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
1043     }
1044 
openPopupIfRequired(MotionEvent me)1045     private boolean openPopupIfRequired(MotionEvent me) {
1046         // Check if we have a popup layout specified first.
1047         if (mPopupLayout == 0) {
1048             return false;
1049         }
1050         if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
1051             return false;
1052         }
1053 
1054         Key popupKey = mKeys[mCurrentKey];
1055         boolean result = onLongPress(popupKey);
1056         if (result) {
1057             mAbortKey = true;
1058             showPreview(NOT_A_KEY);
1059         }
1060         return result;
1061     }
1062 
1063     /**
1064      * Called when a key is long pressed. By default this will open any popup keyboard associated
1065      * with this key through the attributes popupLayout and popupCharacters.
1066      * @param popupKey the key that was long pressed
1067      * @return true if the long press is handled, false otherwise. Subclasses should call the
1068      * method on the base class if the subclass doesn't wish to handle the call.
1069      */
onLongPress(Key popupKey)1070     protected boolean onLongPress(Key popupKey) {
1071         int popupKeyboardId = popupKey.popupResId;
1072 
1073         if (popupKeyboardId != 0) {
1074             mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey);
1075             if (mMiniKeyboardContainer == null) {
1076                 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
1077                         Context.LAYOUT_INFLATER_SERVICE);
1078                 mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null);
1079                 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1080                         com.android.internal.R.id.keyboardView);
1081                 View closeButton = mMiniKeyboardContainer.findViewById(
1082                         com.android.internal.R.id.closeButton);
1083                 if (closeButton != null) closeButton.setOnClickListener(this);
1084                 mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
1085                     public void onKey(int primaryCode, int[] keyCodes) {
1086                         mKeyboardActionListener.onKey(primaryCode, keyCodes);
1087                         dismissPopupKeyboard();
1088                     }
1089 
1090                     public void onText(CharSequence text) {
1091                         mKeyboardActionListener.onText(text);
1092                         dismissPopupKeyboard();
1093                     }
1094 
1095                     public void swipeLeft() { }
1096                     public void swipeRight() { }
1097                     public void swipeUp() { }
1098                     public void swipeDown() { }
1099                     public void onPress(int primaryCode) {
1100                         mKeyboardActionListener.onPress(primaryCode);
1101                     }
1102                     public void onRelease(int primaryCode) {
1103                         mKeyboardActionListener.onRelease(primaryCode);
1104                     }
1105                 });
1106                 //mInputView.setSuggest(mSuggest);
1107                 Keyboard keyboard;
1108                 if (popupKey.popupCharacters != null) {
1109                     keyboard = new Keyboard(getContext(), popupKeyboardId,
1110                             popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
1111                 } else {
1112                     keyboard = new Keyboard(getContext(), popupKeyboardId);
1113                 }
1114                 mMiniKeyboard.setKeyboard(keyboard);
1115                 mMiniKeyboard.setPopupParent(this);
1116                 mMiniKeyboardContainer.measure(
1117                         MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1118                         MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1119 
1120                 mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer);
1121             } else {
1122                 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1123                         com.android.internal.R.id.keyboardView);
1124             }
1125             getLocationInWindow(mCoordinates);
1126             mPopupX = popupKey.x + mPaddingLeft;
1127             mPopupY = popupKey.y + mPaddingTop;
1128             mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth();
1129             mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight();
1130             final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0];
1131             final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1];
1132             mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y);
1133             mMiniKeyboard.setShifted(isShifted());
1134             mPopupKeyboard.setContentView(mMiniKeyboardContainer);
1135             mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth());
1136             mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight());
1137             mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1138             mMiniKeyboardOnScreen = true;
1139             //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me));
1140             invalidateAllKeys();
1141             return true;
1142         }
1143         return false;
1144     }
1145 
1146     @Override
1147     public boolean onHoverEvent(MotionEvent event) {
1148         // If touch exploring is enabled we ignore touch events and transform
1149         // the stream of hover events as touch events. This allows one consistent
1150         // event stream to drive the keyboard since during touch exploring the
1151         // first touch generates only hover events and tapping on the same
1152         // location generates hover and touch events.
1153         if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) {
1154             final int action = event.getAction();
1155             switch (action) {
1156                 case MotionEvent.ACTION_HOVER_ENTER:
1157                     event.setAction(MotionEvent.ACTION_DOWN);
1158                     break;
1159                 case MotionEvent.ACTION_HOVER_MOVE:
1160                     event.setAction(MotionEvent.ACTION_MOVE);
1161                     break;
1162                 case MotionEvent.ACTION_HOVER_EXIT:
1163                     event.setAction(MotionEvent.ACTION_UP);
1164                     break;
1165             }
1166             onTouchEventInternal(event);
1167             event.setAction(action);
1168         }
1169         return super.onHoverEvent(event);
1170     }
1171 
1172     @Override
1173     public boolean onTouchEvent(MotionEvent event) {
1174         // If touch exploring is enabled we ignore touch events and transform
1175         // the stream of hover events as touch events. This allows one consistent
1176         // event stream to drive the keyboard since during touch exploring the
1177         // first touch generates only hover events and tapping on the same
1178         // location generates hover and touch events.
1179         if (mAccessibilityManager.isTouchExplorationEnabled()) {
1180             return true;
1181         }
1182         return onTouchEventInternal(event);
1183     }
1184 
1185     private boolean onTouchEventInternal(MotionEvent me) {
1186         // Convert multi-pointer up/down events to single up/down events to
1187         // deal with the typical multi-pointer behavior of two-thumb typing
1188         final int pointerCount = me.getPointerCount();
1189         final int action = me.getAction();
1190         boolean result = false;
1191         final long now = me.getEventTime();
1192 
1193         if (pointerCount != mOldPointerCount) {
1194             if (pointerCount == 1) {
1195                 // Send a down event for the latest pointer
1196                 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
1197                         me.getX(), me.getY(), me.getMetaState());
1198                 result = onModifiedTouchEvent(down, false);
1199                 down.recycle();
1200                 // If it's an up action, then deliver the up as well.
1201                 if (action == MotionEvent.ACTION_UP) {
1202                     result = onModifiedTouchEvent(me, true);
1203                 }
1204             } else {
1205                 // Send an up event for the last pointer
1206                 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
1207                         mOldPointerX, mOldPointerY, me.getMetaState());
1208                 result = onModifiedTouchEvent(up, true);
1209                 up.recycle();
1210             }
1211         } else {
1212             if (pointerCount == 1) {
1213                 result = onModifiedTouchEvent(me, false);
1214                 mOldPointerX = me.getX();
1215                 mOldPointerY = me.getY();
1216             } else {
1217                 // Don't do anything when 2 pointers are down and moving.
1218                 result = true;
1219             }
1220         }
1221         mOldPointerCount = pointerCount;
1222 
1223         return result;
1224     }
1225 
1226     private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
1227         int touchX = (int) me.getX() - mPaddingLeft;
1228         int touchY = (int) me.getY() - mPaddingTop;
1229         if (touchY >= -mVerticalCorrection)
1230             touchY += mVerticalCorrection;
1231         final int action = me.getAction();
1232         final long eventTime = me.getEventTime();
1233         int keyIndex = getKeyIndices(touchX, touchY, null);
1234         mPossiblePoly = possiblePoly;
1235 
1236         // Track the last few movements to look for spurious swipes.
1237         if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
1238         mSwipeTracker.addMovement(me);
1239 
1240         // Ignore all motion events until a DOWN.
1241         if (mAbortKey
1242                 && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
1243             return true;
1244         }
1245 
1246         if (mGestureDetector.onTouchEvent(me)) {
1247             showPreview(NOT_A_KEY);
1248             mHandler.removeMessages(MSG_REPEAT);
1249             mHandler.removeMessages(MSG_LONGPRESS);
1250             return true;
1251         }
1252 
1253         // Needs to be called after the gesture detector gets a turn, as it may have
1254         // displayed the mini keyboard
1255         if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) {
1256             return true;
1257         }
1258 
1259         switch (action) {
1260             case MotionEvent.ACTION_DOWN:
1261                 mAbortKey = false;
1262                 mStartX = touchX;
1263                 mStartY = touchY;
1264                 mLastCodeX = touchX;
1265                 mLastCodeY = touchY;
1266                 mLastKeyTime = 0;
1267                 mCurrentKeyTime = 0;
1268                 mLastKey = NOT_A_KEY;
1269                 mCurrentKey = keyIndex;
1270                 mDownKey = keyIndex;
1271                 mDownTime = me.getEventTime();
1272                 mLastMoveTime = mDownTime;
1273                 checkMultiTap(eventTime, keyIndex);
1274                 mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
1275                         mKeys[keyIndex].codes[0] : 0);
1276                 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
1277                     mRepeatKeyIndex = mCurrentKey;
1278                     Message msg = mHandler.obtainMessage(MSG_REPEAT);
1279                     mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
1280                     repeatKey();
1281                     // Delivering the key could have caused an abort
1282                     if (mAbortKey) {
1283                         mRepeatKeyIndex = NOT_A_KEY;
1284                         break;
1285                     }
1286                 }
1287                 if (mCurrentKey != NOT_A_KEY) {
1288                     Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1289                     mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1290                 }
1291                 showPreview(keyIndex);
1292                 break;
1293 
1294             case MotionEvent.ACTION_MOVE:
1295                 boolean continueLongPress = false;
1296                 if (keyIndex != NOT_A_KEY) {
1297                     if (mCurrentKey == NOT_A_KEY) {
1298                         mCurrentKey = keyIndex;
1299                         mCurrentKeyTime = eventTime - mDownTime;
1300                     } else {
1301                         if (keyIndex == mCurrentKey) {
1302                             mCurrentKeyTime += eventTime - mLastMoveTime;
1303                             continueLongPress = true;
1304                         } else if (mRepeatKeyIndex == NOT_A_KEY) {
1305                             resetMultiTap();
1306                             mLastKey = mCurrentKey;
1307                             mLastCodeX = mLastX;
1308                             mLastCodeY = mLastY;
1309                             mLastKeyTime =
1310                                     mCurrentKeyTime + eventTime - mLastMoveTime;
1311                             mCurrentKey = keyIndex;
1312                             mCurrentKeyTime = 0;
1313                         }
1314                     }
1315                 }
1316                 if (!continueLongPress) {
1317                     // Cancel old longpress
1318                     mHandler.removeMessages(MSG_LONGPRESS);
1319                     // Start new longpress if key has changed
1320                     if (keyIndex != NOT_A_KEY) {
1321                         Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1322                         mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1323                     }
1324                 }
1325                 showPreview(mCurrentKey);
1326                 mLastMoveTime = eventTime;
1327                 break;
1328 
1329             case MotionEvent.ACTION_UP:
1330                 removeMessages();
1331                 if (keyIndex == mCurrentKey) {
1332                     mCurrentKeyTime += eventTime - mLastMoveTime;
1333                 } else {
1334                     resetMultiTap();
1335                     mLastKey = mCurrentKey;
1336                     mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1337                     mCurrentKey = keyIndex;
1338                     mCurrentKeyTime = 0;
1339                 }
1340                 if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
1341                         && mLastKey != NOT_A_KEY) {
1342                     mCurrentKey = mLastKey;
1343                     touchX = mLastCodeX;
1344                     touchY = mLastCodeY;
1345                 }
1346                 showPreview(NOT_A_KEY);
1347                 Arrays.fill(mKeyIndices, NOT_A_KEY);
1348                 // If we're not on a repeating key (which sends on a DOWN event)
1349                 if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
1350                     detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
1351                 }
1352                 invalidateKey(keyIndex);
1353                 mRepeatKeyIndex = NOT_A_KEY;
1354                 break;
1355             case MotionEvent.ACTION_CANCEL:
1356                 removeMessages();
1357                 dismissPopupKeyboard();
1358                 mAbortKey = true;
1359                 showPreview(NOT_A_KEY);
1360                 invalidateKey(mCurrentKey);
1361                 break;
1362         }
1363         mLastX = touchX;
1364         mLastY = touchY;
1365         return true;
1366     }
1367 
repeatKey()1368     private boolean repeatKey() {
1369         Key key = mKeys[mRepeatKeyIndex];
1370         detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
1371         return true;
1372     }
1373 
swipeRight()1374     protected void swipeRight() {
1375         mKeyboardActionListener.swipeRight();
1376     }
1377 
swipeLeft()1378     protected void swipeLeft() {
1379         mKeyboardActionListener.swipeLeft();
1380     }
1381 
swipeUp()1382     protected void swipeUp() {
1383         mKeyboardActionListener.swipeUp();
1384     }
1385 
swipeDown()1386     protected void swipeDown() {
1387         mKeyboardActionListener.swipeDown();
1388     }
1389 
closing()1390     public void closing() {
1391         if (mPreviewPopup.isShowing()) {
1392             mPreviewPopup.dismiss();
1393         }
1394         removeMessages();
1395 
1396         dismissPopupKeyboard();
1397         mBuffer = null;
1398         mCanvas = null;
1399         mMiniKeyboardCache.clear();
1400     }
1401 
removeMessages()1402     private void removeMessages() {
1403         mHandler.removeMessages(MSG_REPEAT);
1404         mHandler.removeMessages(MSG_LONGPRESS);
1405         mHandler.removeMessages(MSG_SHOW_PREVIEW);
1406     }
1407 
1408     @Override
onDetachedFromWindow()1409     public void onDetachedFromWindow() {
1410         super.onDetachedFromWindow();
1411         closing();
1412     }
1413 
dismissPopupKeyboard()1414     private void dismissPopupKeyboard() {
1415         if (mPopupKeyboard.isShowing()) {
1416             mPopupKeyboard.dismiss();
1417             mMiniKeyboardOnScreen = false;
1418             invalidateAllKeys();
1419         }
1420     }
1421 
handleBack()1422     public boolean handleBack() {
1423         if (mPopupKeyboard.isShowing()) {
1424             dismissPopupKeyboard();
1425             return true;
1426         }
1427         return false;
1428     }
1429 
resetMultiTap()1430     private void resetMultiTap() {
1431         mLastSentIndex = NOT_A_KEY;
1432         mTapCount = 0;
1433         mLastTapTime = -1;
1434         mInMultiTap = false;
1435     }
1436 
checkMultiTap(long eventTime, int keyIndex)1437     private void checkMultiTap(long eventTime, int keyIndex) {
1438         if (keyIndex == NOT_A_KEY) return;
1439         Key key = mKeys[keyIndex];
1440         if (key.codes.length > 1) {
1441             mInMultiTap = true;
1442             if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1443                     && keyIndex == mLastSentIndex) {
1444                 mTapCount = (mTapCount + 1) % key.codes.length;
1445                 return;
1446             } else {
1447                 mTapCount = -1;
1448                 return;
1449             }
1450         }
1451         if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1452             resetMultiTap();
1453         }
1454     }
1455 
1456     private static class SwipeTracker {
1457 
1458         static final int NUM_PAST = 4;
1459         static final int LONGEST_PAST_TIME = 200;
1460 
1461         final float mPastX[] = new float[NUM_PAST];
1462         final float mPastY[] = new float[NUM_PAST];
1463         final long mPastTime[] = new long[NUM_PAST];
1464 
1465         float mYVelocity;
1466         float mXVelocity;
1467 
clear()1468         public void clear() {
1469             mPastTime[0] = 0;
1470         }
1471 
addMovement(MotionEvent ev)1472         public void addMovement(MotionEvent ev) {
1473             long time = ev.getEventTime();
1474             final int N = ev.getHistorySize();
1475             for (int i=0; i<N; i++) {
1476                 addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
1477                         ev.getHistoricalEventTime(i));
1478             }
1479             addPoint(ev.getX(), ev.getY(), time);
1480         }
1481 
addPoint(float x, float y, long time)1482         private void addPoint(float x, float y, long time) {
1483             int drop = -1;
1484             int i;
1485             final long[] pastTime = mPastTime;
1486             for (i=0; i<NUM_PAST; i++) {
1487                 if (pastTime[i] == 0) {
1488                     break;
1489                 } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
1490                     drop = i;
1491                 }
1492             }
1493             if (i == NUM_PAST && drop < 0) {
1494                 drop = 0;
1495             }
1496             if (drop == i) drop--;
1497             final float[] pastX = mPastX;
1498             final float[] pastY = mPastY;
1499             if (drop >= 0) {
1500                 final int start = drop+1;
1501                 final int count = NUM_PAST-drop-1;
1502                 System.arraycopy(pastX, start, pastX, 0, count);
1503                 System.arraycopy(pastY, start, pastY, 0, count);
1504                 System.arraycopy(pastTime, start, pastTime, 0, count);
1505                 i -= (drop+1);
1506             }
1507             pastX[i] = x;
1508             pastY[i] = y;
1509             pastTime[i] = time;
1510             i++;
1511             if (i < NUM_PAST) {
1512                 pastTime[i] = 0;
1513             }
1514         }
1515 
computeCurrentVelocity(int units)1516         public void computeCurrentVelocity(int units) {
1517             computeCurrentVelocity(units, Float.MAX_VALUE);
1518         }
1519 
computeCurrentVelocity(int units, float maxVelocity)1520         public void computeCurrentVelocity(int units, float maxVelocity) {
1521             final float[] pastX = mPastX;
1522             final float[] pastY = mPastY;
1523             final long[] pastTime = mPastTime;
1524 
1525             final float oldestX = pastX[0];
1526             final float oldestY = pastY[0];
1527             final long oldestTime = pastTime[0];
1528             float accumX = 0;
1529             float accumY = 0;
1530             int N=0;
1531             while (N < NUM_PAST) {
1532                 if (pastTime[N] == 0) {
1533                     break;
1534                 }
1535                 N++;
1536             }
1537 
1538             for (int i=1; i < N; i++) {
1539                 final int dur = (int)(pastTime[i] - oldestTime);
1540                 if (dur == 0) continue;
1541                 float dist = pastX[i] - oldestX;
1542                 float vel = (dist/dur) * units;   // pixels/frame.
1543                 if (accumX == 0) accumX = vel;
1544                 else accumX = (accumX + vel) * .5f;
1545 
1546                 dist = pastY[i] - oldestY;
1547                 vel = (dist/dur) * units;   // pixels/frame.
1548                 if (accumY == 0) accumY = vel;
1549                 else accumY = (accumY + vel) * .5f;
1550             }
1551             mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
1552                     : Math.min(accumX, maxVelocity);
1553             mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
1554                     : Math.min(accumY, maxVelocity);
1555         }
1556 
1557         public float getXVelocity() {
1558             return mXVelocity;
1559         }
1560 
1561         public float getYVelocity() {
1562             return mYVelocity;
1563         }
1564     }
1565 }
1566