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