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