• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.inputmethod.keyboard;
18 
19 import android.animation.AnimatorInflater;
20 import android.animation.ObjectAnimator;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Paint.Align;
28 import android.graphics.Typeface;
29 import android.graphics.drawable.Drawable;
30 import android.os.Message;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.ViewConfiguration;
38 import android.view.ViewGroup;
39 import android.view.inputmethod.InputMethodSubtype;
40 import android.widget.PopupWindow;
41 
42 import com.android.inputmethod.accessibility.AccessibilityUtils;
43 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
44 import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
45 import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
46 import com.android.inputmethod.latin.LatinIME;
47 import com.android.inputmethod.latin.LatinImeLogger;
48 import com.android.inputmethod.latin.R;
49 import com.android.inputmethod.latin.ResearchLogger;
50 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
51 import com.android.inputmethod.latin.StringUtils;
52 import com.android.inputmethod.latin.SubtypeLocale;
53 import com.android.inputmethod.latin.Utils;
54 import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
55 import com.android.inputmethod.latin.define.ProductionFlag;
56 
57 import java.util.Locale;
58 import java.util.WeakHashMap;
59 
60 /**
61  * A view that is responsible for detecting key presses and touch movements.
62  *
63  * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
64  * @attr ref R.styleable#KeyboardView_verticalCorrection
65  * @attr ref R.styleable#KeyboardView_popupLayout
66  */
67 public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
68         SuddenJumpingTouchEventHandler.ProcessMotionEvent {
69     private static final String TAG = LatinKeyboardView.class.getSimpleName();
70 
71     // TODO: Kill process when the usability study mode was changed.
72     private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
73 
74     /** Listener for {@link KeyboardActionListener}. */
75     private KeyboardActionListener mKeyboardActionListener;
76 
77     /* Space key and its icons */
78     private Key mSpaceKey;
79     private Drawable mSpaceIcon;
80     // Stuff to draw language name on spacebar.
81     private final int mLanguageOnSpacebarFinalAlpha;
82     private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
83     private static final int ALPHA_OPAQUE = 255;
84     private boolean mNeedsToDisplayLanguage;
85     private boolean mHasMultipleEnabledIMEsOrSubtypes;
86     private int mLanguageOnSpacebarAnimAlpha = ALPHA_OPAQUE;
87     private final float mSpacebarTextRatio;
88     private float mSpacebarTextSize;
89     private final int mSpacebarTextColor;
90     private final int mSpacebarTextShadowColor;
91     // The minimum x-scale to fit the language name on spacebar.
92     private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
93     // Stuff to draw auto correction LED on spacebar.
94     private boolean mAutoCorrectionSpacebarLedOn;
95     private final boolean mAutoCorrectionSpacebarLedEnabled;
96     private final Drawable mAutoCorrectionSpacebarLedIcon;
97     private static final int SPACE_LED_LENGTH_PERCENT = 80;
98 
99     // Stuff to draw altCodeWhileTyping keys.
100     private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
101     private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
102     private int mAltCodeKeyWhileTypingAnimAlpha = ALPHA_OPAQUE;
103 
104     // More keys keyboard
105     private PopupWindow mMoreKeysWindow;
106     private MoreKeysPanel mMoreKeysPanel;
107     private int mMoreKeysPanelPointerTrackerId;
108     private final WeakHashMap<Key, MoreKeysPanel> mMoreKeysPanelCache =
109             new WeakHashMap<Key, MoreKeysPanel>();
110     private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
111 
112     private final PointerTrackerParams mPointerTrackerParams;
113     private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
114 
115     protected KeyDetector mKeyDetector;
116     private boolean mHasDistinctMultitouch;
117     private int mOldPointerCount = 1;
118     private Key mOldKey;
119 
120     private final KeyTimerHandler mKeyTimerHandler;
121 
122     private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
123             implements TimerProxy {
124         private static final int MSG_REPEAT_KEY = 1;
125         private static final int MSG_LONGPRESS_KEY = 2;
126         private static final int MSG_DOUBLE_TAP = 3;
127         private static final int MSG_TYPING_STATE_EXPIRED = 4;
128 
129         private final KeyTimerParams mParams;
130         private boolean mInKeyRepeat;
131 
KeyTimerHandler(LatinKeyboardView outerInstance, KeyTimerParams params)132         public KeyTimerHandler(LatinKeyboardView outerInstance, KeyTimerParams params) {
133             super(outerInstance);
134             mParams = params;
135         }
136 
137         @Override
handleMessage(Message msg)138         public void handleMessage(Message msg) {
139             final LatinKeyboardView keyboardView = getOuterInstance();
140             final PointerTracker tracker = (PointerTracker) msg.obj;
141             switch (msg.what) {
142             case MSG_REPEAT_KEY:
143                 tracker.onRegisterKey(tracker.getKey());
144                 startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
145                 break;
146             case MSG_LONGPRESS_KEY:
147                 if (tracker != null) {
148                     keyboardView.openMoreKeysKeyboardIfRequired(tracker.getKey(), tracker);
149                 } else {
150                     KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
151                 }
152                 break;
153             case MSG_TYPING_STATE_EXPIRED:
154                 cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
155                         keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
156                 break;
157             }
158         }
159 
startKeyRepeatTimer(PointerTracker tracker, long delay)160         private void startKeyRepeatTimer(PointerTracker tracker, long delay) {
161             sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, tracker), delay);
162         }
163 
164         @Override
startKeyRepeatTimer(PointerTracker tracker)165         public void startKeyRepeatTimer(PointerTracker tracker) {
166             mInKeyRepeat = true;
167             startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
168         }
169 
cancelKeyRepeatTimer()170         public void cancelKeyRepeatTimer() {
171             mInKeyRepeat = false;
172             removeMessages(MSG_REPEAT_KEY);
173         }
174 
isInKeyRepeat()175         public boolean isInKeyRepeat() {
176             return mInKeyRepeat;
177         }
178 
179         @Override
startLongPressTimer(int code)180         public void startLongPressTimer(int code) {
181             cancelLongPressTimer();
182             final int delay;
183             switch (code) {
184             case Keyboard.CODE_SHIFT:
185                 delay = mParams.mLongPressShiftKeyTimeout;
186                 break;
187             default:
188                 delay = 0;
189                 break;
190             }
191             if (delay > 0) {
192                 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, code, 0), delay);
193             }
194         }
195 
196         @Override
startLongPressTimer(PointerTracker tracker)197         public void startLongPressTimer(PointerTracker tracker) {
198             cancelLongPressTimer();
199             if (tracker == null) {
200                 return;
201             }
202             final Key key = tracker.getKey();
203             final int delay;
204             switch (key.mCode) {
205             case Keyboard.CODE_SHIFT:
206                 delay = mParams.mLongPressShiftKeyTimeout;
207                 break;
208             default:
209                 if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
210                     // We use longer timeout for sliding finger input started from the symbols
211                     // mode key.
212                     delay = mParams.mLongPressKeyTimeout * 3;
213                 } else {
214                     delay = mParams.mLongPressKeyTimeout;
215                 }
216                 break;
217             }
218             if (delay > 0) {
219                 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
220             }
221         }
222 
223         @Override
cancelLongPressTimer()224         public void cancelLongPressTimer() {
225             removeMessages(MSG_LONGPRESS_KEY);
226         }
227 
cancelAndStartAnimators(final ObjectAnimator animatorToCancel, final ObjectAnimator animatorToStart)228         public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
229                 final ObjectAnimator animatorToStart) {
230             float startFraction = 0.0f;
231             if (animatorToCancel.isStarted()) {
232                 animatorToCancel.cancel();
233                 startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
234             }
235             final long startTime = (long)(animatorToStart.getDuration() * startFraction);
236             animatorToStart.start();
237             animatorToStart.setCurrentPlayTime(startTime);
238         }
239 
240         @Override
startTypingStateTimer()241         public void startTypingStateTimer() {
242             final boolean isTyping = isTypingState();
243             removeMessages(MSG_TYPING_STATE_EXPIRED);
244             sendMessageDelayed(
245                     obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
246             if (isTyping) {
247                 return;
248             }
249             final LatinKeyboardView keyboardView = getOuterInstance();
250             cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
251                     keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
252         }
253 
254         @Override
isTypingState()255         public boolean isTypingState() {
256             return hasMessages(MSG_TYPING_STATE_EXPIRED);
257         }
258 
259         @Override
startDoubleTapTimer()260         public void startDoubleTapTimer() {
261             sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP),
262                     ViewConfiguration.getDoubleTapTimeout());
263         }
264 
265         @Override
cancelDoubleTapTimer()266         public void cancelDoubleTapTimer() {
267             removeMessages(MSG_DOUBLE_TAP);
268         }
269 
270         @Override
isInDoubleTapTimeout()271         public boolean isInDoubleTapTimeout() {
272             return hasMessages(MSG_DOUBLE_TAP);
273         }
274 
275         @Override
cancelKeyTimers()276         public void cancelKeyTimers() {
277             cancelKeyRepeatTimer();
278             cancelLongPressTimer();
279         }
280 
cancelAllMessages()281         public void cancelAllMessages() {
282             cancelKeyTimers();
283         }
284     }
285 
286     public static class PointerTrackerParams {
287         public final boolean mSlidingKeyInputEnabled;
288         public final int mTouchNoiseThresholdTime;
289         public final float mTouchNoiseThresholdDistance;
290 
291         public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
292 
PointerTrackerParams()293         private PointerTrackerParams() {
294             mSlidingKeyInputEnabled = false;
295             mTouchNoiseThresholdTime =0;
296             mTouchNoiseThresholdDistance = 0;
297         }
298 
PointerTrackerParams(TypedArray latinKeyboardViewAttr)299         public PointerTrackerParams(TypedArray latinKeyboardViewAttr) {
300             mSlidingKeyInputEnabled = latinKeyboardViewAttr.getBoolean(
301                     R.styleable.LatinKeyboardView_slidingKeyInputEnable, false);
302             mTouchNoiseThresholdTime = latinKeyboardViewAttr.getInt(
303                     R.styleable.LatinKeyboardView_touchNoiseThresholdTime, 0);
304             mTouchNoiseThresholdDistance = latinKeyboardViewAttr.getDimension(
305                     R.styleable.LatinKeyboardView_touchNoiseThresholdDistance, 0);
306         }
307     }
308 
309     static class KeyTimerParams {
310         public final int mKeyRepeatStartTimeout;
311         public final int mKeyRepeatInterval;
312         public final int mLongPressKeyTimeout;
313         public final int mLongPressShiftKeyTimeout;
314         public final int mIgnoreAltCodeKeyTimeout;
315 
KeyTimerParams(TypedArray latinKeyboardViewAttr)316         public KeyTimerParams(TypedArray latinKeyboardViewAttr) {
317             mKeyRepeatStartTimeout = latinKeyboardViewAttr.getInt(
318                     R.styleable.LatinKeyboardView_keyRepeatStartTimeout, 0);
319             mKeyRepeatInterval = latinKeyboardViewAttr.getInt(
320                     R.styleable.LatinKeyboardView_keyRepeatInterval, 0);
321             mLongPressKeyTimeout = latinKeyboardViewAttr.getInt(
322                     R.styleable.LatinKeyboardView_longPressKeyTimeout, 0);
323             mLongPressShiftKeyTimeout = latinKeyboardViewAttr.getInt(
324                     R.styleable.LatinKeyboardView_longPressShiftKeyTimeout, 0);
325             mIgnoreAltCodeKeyTimeout = latinKeyboardViewAttr.getInt(
326                     R.styleable.LatinKeyboardView_ignoreAltCodeKeyTimeout, 0);
327         }
328     }
329 
LatinKeyboardView(Context context, AttributeSet attrs)330     public LatinKeyboardView(Context context, AttributeSet attrs) {
331         this(context, attrs, R.attr.latinKeyboardViewStyle);
332     }
333 
LatinKeyboardView(Context context, AttributeSet attrs, int defStyle)334     public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
335         super(context, attrs, defStyle);
336 
337         mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
338 
339         mHasDistinctMultitouch = context.getPackageManager()
340                 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
341         final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
342                 Utils.getDeviceOverrideValue(context.getResources(),
343                         R.array.phantom_sudden_move_event_device_list, "false"));
344         PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack);
345 
346         final TypedArray a = context.obtainStyledAttributes(
347                 attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
348         mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
349                 R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
350         mAutoCorrectionSpacebarLedIcon = a.getDrawable(
351                 R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
352         mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
353                 1000, 1000, 1) / 1000.0f;
354         mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
355         mSpacebarTextShadowColor = a.getColor(
356                 R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
357         mLanguageOnSpacebarFinalAlpha = a.getInt(
358                 R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha, ALPHA_OPAQUE);
359         final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
360                 R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
361         final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
362                 R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
363         final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
364                 R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
365 
366         final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
367         mPointerTrackerParams = new PointerTrackerParams(a);
368 
369         final float keyHysteresisDistance = a.getDimension(
370                 R.styleable.LatinKeyboardView_keyHysteresisDistance, 0);
371         mKeyDetector = new KeyDetector(keyHysteresisDistance);
372         mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
373         mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
374                 R.styleable.LatinKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
375         a.recycle();
376 
377         PointerTracker.setParameters(mPointerTrackerParams);
378 
379         mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
380                 languageOnSpacebarFadeoutAnimatorResId, this);
381         mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
382                 altCodeKeyWhileTypingFadeoutAnimatorResId, this);
383         mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
384                 altCodeKeyWhileTypingFadeinAnimatorResId, this);
385     }
386 
loadObjectAnimator(int resId, Object target)387     private ObjectAnimator loadObjectAnimator(int resId, Object target) {
388         if (resId == 0) return null;
389         final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
390                 getContext(), resId);
391         if (animator != null) {
392             animator.setTarget(target);
393         }
394         return animator;
395     }
396 
397     // Getter/setter methods for {@link ObjectAnimator}.
getLanguageOnSpacebarAnimAlpha()398     public int getLanguageOnSpacebarAnimAlpha() {
399         return mLanguageOnSpacebarAnimAlpha;
400     }
401 
setLanguageOnSpacebarAnimAlpha(int alpha)402     public void setLanguageOnSpacebarAnimAlpha(int alpha) {
403         mLanguageOnSpacebarAnimAlpha = alpha;
404         invalidateKey(mSpaceKey);
405     }
406 
getAltCodeKeyWhileTypingAnimAlpha()407     public int getAltCodeKeyWhileTypingAnimAlpha() {
408         return mAltCodeKeyWhileTypingAnimAlpha;
409     }
410 
setAltCodeKeyWhileTypingAnimAlpha(int alpha)411     public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) {
412         mAltCodeKeyWhileTypingAnimAlpha = alpha;
413         updateAltCodeKeyWhileTyping();
414     }
415 
setKeyboardActionListener(KeyboardActionListener listener)416     public void setKeyboardActionListener(KeyboardActionListener listener) {
417         mKeyboardActionListener = listener;
418         PointerTracker.setKeyboardActionListener(listener);
419     }
420 
421     /**
422      * Returns the {@link KeyboardActionListener} object.
423      * @return the listener attached to this keyboard
424      */
425     @Override
getKeyboardActionListener()426     public KeyboardActionListener getKeyboardActionListener() {
427         return mKeyboardActionListener;
428     }
429 
430     @Override
getKeyDetector()431     public KeyDetector getKeyDetector() {
432         return mKeyDetector;
433     }
434 
435     @Override
getDrawingProxy()436     public DrawingProxy getDrawingProxy() {
437         return this;
438     }
439 
440     @Override
getTimerProxy()441     public TimerProxy getTimerProxy() {
442         return mKeyTimerHandler;
443     }
444 
445     /**
446      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
447      * view will re-layout itself to accommodate the keyboard.
448      * @see Keyboard
449      * @see #getKeyboard()
450      * @param keyboard the keyboard to display in this view
451      */
452     @Override
setKeyboard(Keyboard keyboard)453     public void setKeyboard(Keyboard keyboard) {
454         // Remove any pending messages, except dismissing preview
455         mKeyTimerHandler.cancelKeyTimers();
456         super.setKeyboard(keyboard);
457         mKeyDetector.setKeyboard(
458                 keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
459         PointerTracker.setKeyDetector(mKeyDetector);
460         mTouchScreenRegulator.setKeyboard(keyboard);
461         mMoreKeysPanelCache.clear();
462 
463         mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
464         mSpaceIcon = (mSpaceKey != null)
465                 ? mSpaceKey.getIcon(keyboard.mIconsSet, ALPHA_OPAQUE) : null;
466         final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
467         mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
468         if (ProductionFlag.IS_EXPERIMENTAL) {
469             ResearchLogger.latinKeyboardView_setKeyboard(keyboard);
470         }
471 
472         // This always needs to be set since the accessibility state can
473         // potentially change without the keyboard being set again.
474         AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
475     }
476 
477     /**
478      * Returns whether the device has distinct multi-touch panel.
479      * @return true if the device has distinct multi-touch panel.
480      */
hasDistinctMultitouch()481     public boolean hasDistinctMultitouch() {
482         return mHasDistinctMultitouch;
483     }
484 
setDistinctMultitouch(boolean hasDistinctMultitouch)485     public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
486         mHasDistinctMultitouch = hasDistinctMultitouch;
487     }
488 
489     /**
490      * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
491      * codes for adjacent keys.  When disabled, only the primary key code will be
492      * reported.
493      * @param enabled whether or not the proximity correction is enabled
494      */
setProximityCorrectionEnabled(boolean enabled)495     public void setProximityCorrectionEnabled(boolean enabled) {
496         mKeyDetector.setProximityCorrectionEnabled(enabled);
497     }
498 
499     /**
500      * Returns true if proximity correction is enabled.
501      */
isProximityCorrectionEnabled()502     public boolean isProximityCorrectionEnabled() {
503         return mKeyDetector.isProximityCorrectionEnabled();
504     }
505 
506     @Override
cancelAllMessages()507     public void cancelAllMessages() {
508         mKeyTimerHandler.cancelAllMessages();
509         super.cancelAllMessages();
510     }
511 
openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker)512     private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
513         // Check if we have a popup layout specified first.
514         if (mMoreKeysLayout == 0) {
515             return false;
516         }
517 
518         // Check if we are already displaying popup panel.
519         if (mMoreKeysPanel != null)
520             return false;
521         if (parentKey == null)
522             return false;
523         return onLongPress(parentKey, tracker);
524     }
525 
526     // This default implementation returns a more keys panel.
onCreateMoreKeysPanel(Key parentKey)527     protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
528         if (parentKey.mMoreKeys == null)
529             return null;
530 
531         final View container = LayoutInflater.from(getContext()).inflate(mMoreKeysLayout, null);
532         if (container == null)
533             throw new NullPointerException();
534 
535         final MoreKeysKeyboardView moreKeysKeyboardView =
536                 (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
537         final Keyboard moreKeysKeyboard = new MoreKeysKeyboard.Builder(container, parentKey, this)
538                 .build();
539         moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
540         container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
541 
542         return moreKeysKeyboardView;
543     }
544 
545     /**
546      * Called when a key is long pressed. By default this will open more keys keyboard associated
547      * with this key.
548      * @param parentKey the key that was long pressed
549      * @param tracker the pointer tracker which pressed the parent key
550      * @return true if the long press is handled, false otherwise. Subclasses should call the
551      * method on the base class if the subclass doesn't wish to handle the call.
552      */
onLongPress(Key parentKey, PointerTracker tracker)553     protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
554         if (ProductionFlag.IS_EXPERIMENTAL) {
555             ResearchLogger.latinKeyboardView_onLongPress();
556         }
557         final int primaryCode = parentKey.mCode;
558         if (parentKey.hasEmbeddedMoreKey()) {
559             final int embeddedCode = parentKey.mMoreKeys[0].mCode;
560             tracker.onLongPressed();
561             invokeCodeInput(embeddedCode);
562             invokeReleaseKey(primaryCode);
563             KeyboardSwitcher.getInstance().hapticAndAudioFeedback(primaryCode);
564             return true;
565         }
566         if (primaryCode == Keyboard.CODE_SPACE || primaryCode == Keyboard.CODE_LANGUAGE_SWITCH) {
567             // Long pressing the space key invokes IME switcher dialog.
568             if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
569                 tracker.onLongPressed();
570                 invokeReleaseKey(primaryCode);
571                 return true;
572             }
573         }
574         return openMoreKeysPanel(parentKey, tracker);
575     }
576 
invokeCustomRequest(int code)577     private boolean invokeCustomRequest(int code) {
578         return mKeyboardActionListener.onCustomRequest(code);
579     }
580 
invokeCodeInput(int primaryCode)581     private void invokeCodeInput(int primaryCode) {
582         mKeyboardActionListener.onCodeInput(primaryCode,
583                 KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
584                 KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
585     }
586 
invokeReleaseKey(int primaryCode)587     private void invokeReleaseKey(int primaryCode) {
588         mKeyboardActionListener.onReleaseKey(primaryCode, false);
589     }
590 
openMoreKeysPanel(Key parentKey, PointerTracker tracker)591     private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
592         MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
593         if (moreKeysPanel == null) {
594             moreKeysPanel = onCreateMoreKeysPanel(parentKey);
595             if (moreKeysPanel == null)
596                 return false;
597             mMoreKeysPanelCache.put(parentKey, moreKeysPanel);
598         }
599         if (mMoreKeysWindow == null) {
600             mMoreKeysWindow = new PopupWindow(getContext());
601             mMoreKeysWindow.setBackgroundDrawable(null);
602             mMoreKeysWindow.setAnimationStyle(R.style.MoreKeysKeyboardAnimation);
603         }
604         mMoreKeysPanel = moreKeysPanel;
605         mMoreKeysPanelPointerTrackerId = tracker.mPointerId;
606 
607         final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview();
608         // The more keys keyboard is usually horizontally aligned with the center of the parent key.
609         // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
610         // keys keyboard is placed at the touch point of the parent key.
611         final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
612                 ? tracker.getLastX()
613                 : parentKey.mX + parentKey.mWidth / 2;
614         // The more keys keyboard is usually vertically aligned with the top edge of the parent key
615         // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
616         // aligned with the bottom edge of the visible part of the key preview.
617         final int pointY = parentKey.mY + (keyPreviewEnabled
618                 ? mKeyPreviewDrawParams.mPreviewVisibleOffset
619                 : -parentKey.mVerticalGap);
620         moreKeysPanel.showMoreKeysPanel(
621                 this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
622         final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
623         final int translatedY = moreKeysPanel.translateY(tracker.getLastY());
624         tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
625         dimEntireKeyboard(true);
626         return true;
627     }
628 
isInSlidingKeyInput()629     public boolean isInSlidingKeyInput() {
630         if (mMoreKeysPanel != null) {
631             return true;
632         } else {
633             return PointerTracker.isAnyInSlidingKeyInput();
634         }
635     }
636 
getPointerCount()637     public int getPointerCount() {
638         return mOldPointerCount;
639     }
640 
641     @Override
onTouchEvent(MotionEvent me)642     public boolean onTouchEvent(MotionEvent me) {
643         if (getKeyboard() == null) {
644             return false;
645         }
646         return mTouchScreenRegulator.onTouchEvent(me);
647     }
648 
649     @Override
processMotionEvent(MotionEvent me)650     public boolean processMotionEvent(MotionEvent me) {
651         final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
652         final int action = me.getActionMasked();
653         final int pointerCount = me.getPointerCount();
654         final int oldPointerCount = mOldPointerCount;
655         mOldPointerCount = pointerCount;
656 
657         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
658         // If the device does not have distinct multi-touch support panel, ignore all multi-touch
659         // events except a transition from/to single-touch.
660         if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
661             return true;
662         }
663 
664         final long eventTime = me.getEventTime();
665         final int index = me.getActionIndex();
666         final int id = me.getPointerId(index);
667         final int x, y;
668         if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
669             x = mMoreKeysPanel.translateX((int)me.getX(index));
670             y = mMoreKeysPanel.translateY((int)me.getY(index));
671         } else {
672             x = (int)me.getX(index);
673             y = (int)me.getY(index);
674         }
675         if (ENABLE_USABILITY_STUDY_LOG) {
676             final String eventTag;
677             switch (action) {
678                 case MotionEvent.ACTION_UP:
679                     eventTag = "[Up]";
680                     break;
681                 case MotionEvent.ACTION_DOWN:
682                     eventTag = "[Down]";
683                     break;
684                 case MotionEvent.ACTION_POINTER_UP:
685                     eventTag = "[PointerUp]";
686                     break;
687                 case MotionEvent.ACTION_POINTER_DOWN:
688                     eventTag = "[PointerDown]";
689                     break;
690                 case MotionEvent.ACTION_MOVE: // Skip this as being logged below
691                     eventTag = "";
692                     break;
693                 default:
694                     eventTag = "[Action" + action + "]";
695                     break;
696             }
697             if (!TextUtils.isEmpty(eventTag)) {
698                 final float size = me.getSize(index);
699                 final float pressure = me.getPressure(index);
700                 UsabilityStudyLogUtils.getInstance().write(
701                         eventTag + eventTime + "," + id + "," + x + "," + y + ","
702                         + size + "," + pressure);
703             }
704         }
705         if (ProductionFlag.IS_EXPERIMENTAL) {
706             ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime, index, id,
707                     x, y);
708         }
709 
710         if (mKeyTimerHandler.isInKeyRepeat()) {
711             final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
712             // Key repeating timer will be canceled if 2 or more keys are in action, and current
713             // event (UP or DOWN) is non-modifier key.
714             if (pointerCount > 1 && !tracker.isModifier()) {
715                 mKeyTimerHandler.cancelKeyRepeatTimer();
716             }
717             // Up event will pass through.
718         }
719 
720         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
721         // Translate mutli-touch event to single-touch events on the device that has no distinct
722         // multi-touch panel.
723         if (nonDistinctMultitouch) {
724             // Use only main (id=0) pointer tracker.
725             final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
726             if (pointerCount == 1 && oldPointerCount == 2) {
727                 // Multi-touch to single touch transition.
728                 // Send a down event for the latest pointer if the key is different from the
729                 // previous key.
730                 final Key newKey = tracker.getKeyOn(x, y);
731                 if (mOldKey != newKey) {
732                     tracker.onDownEvent(x, y, eventTime, this);
733                     if (action == MotionEvent.ACTION_UP)
734                         tracker.onUpEvent(x, y, eventTime);
735                 }
736             } else if (pointerCount == 2 && oldPointerCount == 1) {
737                 // Single-touch to multi-touch transition.
738                 // Send an up event for the last pointer.
739                 final int lastX = tracker.getLastX();
740                 final int lastY = tracker.getLastY();
741                 mOldKey = tracker.getKeyOn(lastX, lastY);
742                 tracker.onUpEvent(lastX, lastY, eventTime);
743             } else if (pointerCount == 1 && oldPointerCount == 1) {
744                 tracker.processMotionEvent(action, x, y, eventTime, this);
745             } else {
746                 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
747                         + " (old " + oldPointerCount + ")");
748             }
749             return true;
750         }
751 
752         if (action == MotionEvent.ACTION_MOVE) {
753             for (int i = 0; i < pointerCount; i++) {
754                 final int pointerId = me.getPointerId(i);
755                 final PointerTracker tracker = PointerTracker.getPointerTracker(
756                         pointerId, this);
757                 final int px, py;
758                 if (mMoreKeysPanel != null
759                         && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
760                     px = mMoreKeysPanel.translateX((int)me.getX(i));
761                     py = mMoreKeysPanel.translateY((int)me.getY(i));
762                 } else {
763                     px = (int)me.getX(i);
764                     py = (int)me.getY(i);
765                 }
766                 tracker.onMoveEvent(px, py, eventTime);
767                 if (ENABLE_USABILITY_STUDY_LOG) {
768                     final float pointerSize = me.getSize(i);
769                     final float pointerPressure = me.getPressure(i);
770                     UsabilityStudyLogUtils.getInstance().write("[Move]"  + eventTime + ","
771                             + pointerId + "," + px + "," + py + ","
772                             + pointerSize + "," + pointerPressure);
773                 }
774                 if (ProductionFlag.IS_EXPERIMENTAL) {
775                     ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime,
776                             i, pointerId, px, py);
777                 }
778             }
779         } else {
780             final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
781             tracker.processMotionEvent(action, x, y, eventTime, this);
782         }
783 
784         return true;
785     }
786 
787     @Override
closing()788     public void closing() {
789         super.closing();
790         dismissMoreKeysPanel();
791         mMoreKeysPanelCache.clear();
792     }
793 
794     @Override
dismissMoreKeysPanel()795     public boolean dismissMoreKeysPanel() {
796         if (mMoreKeysWindow != null && mMoreKeysWindow.isShowing()) {
797             mMoreKeysWindow.dismiss();
798             mMoreKeysPanel = null;
799             mMoreKeysPanelPointerTrackerId = -1;
800             dimEntireKeyboard(false);
801             return true;
802         }
803         return false;
804     }
805 
806     @Override
draw(Canvas c)807     public void draw(Canvas c) {
808         Utils.GCUtils.getInstance().reset();
809         boolean tryGC = true;
810         for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
811             try {
812                 super.draw(c);
813                 tryGC = false;
814             } catch (OutOfMemoryError e) {
815                 tryGC = Utils.GCUtils.getInstance().tryGCOrWait(TAG, e);
816             }
817         }
818     }
819 
820     /**
821      * Receives hover events from the input framework.
822      *
823      * @param event The motion event to be dispatched.
824      * @return {@code true} if the event was handled by the view, {@code false}
825      *         otherwise
826      */
827     @Override
dispatchHoverEvent(MotionEvent event)828     public boolean dispatchHoverEvent(MotionEvent event) {
829         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
830             final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
831             return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
832         }
833 
834         // Reflection doesn't support calling superclass methods.
835         return false;
836     }
837 
updateShortcutKey(boolean available)838     public void updateShortcutKey(boolean available) {
839         final Keyboard keyboard = getKeyboard();
840         if (keyboard == null) return;
841         final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
842         if (shortcutKey == null) return;
843         shortcutKey.setEnabled(available);
844         invalidateKey(shortcutKey);
845     }
846 
updateAltCodeKeyWhileTyping()847     private void updateAltCodeKeyWhileTyping() {
848         final Keyboard keyboard = getKeyboard();
849         if (keyboard == null) return;
850         for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
851             invalidateKey(key);
852         }
853     }
854 
startDisplayLanguageOnSpacebar(boolean subtypeChanged, boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes)855     public void startDisplayLanguageOnSpacebar(boolean subtypeChanged,
856             boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) {
857         mNeedsToDisplayLanguage = needsToDisplayLanguage;
858         mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
859         final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
860         if (animator == null) {
861             mNeedsToDisplayLanguage = false;
862         } else {
863             if (subtypeChanged && needsToDisplayLanguage) {
864                 setLanguageOnSpacebarAnimAlpha(ALPHA_OPAQUE);
865                 if (animator.isStarted()) {
866                     animator.cancel();
867                 }
868                 animator.start();
869             } else {
870                 if (!animator.isStarted()) {
871                     mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
872                 }
873             }
874         }
875         invalidateKey(mSpaceKey);
876     }
877 
updateAutoCorrectionState(boolean isAutoCorrection)878     public void updateAutoCorrectionState(boolean isAutoCorrection) {
879         if (!mAutoCorrectionSpacebarLedEnabled) return;
880         mAutoCorrectionSpacebarLedOn = isAutoCorrection;
881         invalidateKey(mSpaceKey);
882     }
883 
884     @Override
onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params)885     protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
886         if (key.altCodeWhileTyping() && key.isEnabled()) {
887             params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
888         }
889         if (key.mCode == Keyboard.CODE_SPACE) {
890             drawSpacebar(key, canvas, paint);
891             // Whether space key needs to show the "..." popup hint for special purposes
892             if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
893                 drawKeyPopupHint(key, canvas, paint, params);
894             }
895         } else if (key.mCode == Keyboard.CODE_LANGUAGE_SWITCH) {
896             super.onDrawKeyTopVisuals(key, canvas, paint, params);
897             drawKeyPopupHint(key, canvas, paint, params);
898         } else {
899             super.onDrawKeyTopVisuals(key, canvas, paint, params);
900         }
901     }
902 
fitsTextIntoWidth(final int width, String text, Paint paint)903     private boolean fitsTextIntoWidth(final int width, String text, Paint paint) {
904         paint.setTextScaleX(1.0f);
905         final float textWidth = getLabelWidth(text, paint);
906         if (textWidth < width) return true;
907 
908         final float scaleX = width / textWidth;
909         if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) return false;
910 
911         paint.setTextScaleX(scaleX);
912         return getLabelWidth(text, paint) < width;
913     }
914 
915     // Layout language name on spacebar.
layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype, final int width)916     private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype,
917             final int width) {
918         // Choose appropriate language name to fit into the width.
919         String text = getFullDisplayName(subtype, getResources());
920         if (fitsTextIntoWidth(width, text, paint)) {
921             return text;
922         }
923 
924         text = getMiddleDisplayName(subtype);
925         if (fitsTextIntoWidth(width, text, paint)) {
926             return text;
927         }
928 
929         text = getShortDisplayName(subtype);
930         if (fitsTextIntoWidth(width, text, paint)) {
931             return text;
932         }
933 
934         return "";
935     }
936 
drawSpacebar(Key key, Canvas canvas, Paint paint)937     private void drawSpacebar(Key key, Canvas canvas, Paint paint) {
938         final int width = key.mWidth;
939         final int height = key.mHeight;
940 
941         // If input language are explicitly selected.
942         if (mNeedsToDisplayLanguage) {
943             paint.setTextAlign(Align.CENTER);
944             paint.setTypeface(Typeface.DEFAULT);
945             paint.setTextSize(mSpacebarTextSize);
946             final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
947             final String language = layoutLanguageOnSpacebar(paint, subtype, width);
948             // Draw language text with shadow
949             final float descent = paint.descent();
950             final float textHeight = -paint.ascent() + descent;
951             final float baseline = height / 2 + textHeight / 2;
952             paint.setColor(mSpacebarTextShadowColor);
953             paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
954             canvas.drawText(language, width / 2, baseline - descent - 1, paint);
955             paint.setColor(mSpacebarTextColor);
956             paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
957             canvas.drawText(language, width / 2, baseline - descent, paint);
958         }
959 
960         // Draw the spacebar icon at the bottom
961         if (mAutoCorrectionSpacebarLedOn) {
962             final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
963             final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
964             int x = (width - iconWidth) / 2;
965             int y = height - iconHeight;
966             drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight);
967         } else if (mSpaceIcon != null) {
968             final int iconWidth = mSpaceIcon.getIntrinsicWidth();
969             final int iconHeight = mSpaceIcon.getIntrinsicHeight();
970             int x = (width - iconWidth) / 2;
971             int y = height - iconHeight;
972             drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight);
973         }
974     }
975 
976     // InputMethodSubtype's display name for spacebar text in its locale.
977     //        isAdditionalSubtype (T=true, F=false)
978     // locale layout | Short  Middle      Full
979     // ------ ------ - ---- --------- ----------------------
980     //  en_US qwerty F  En  English   English (US)           exception
981     //  en_GB qwerty F  En  English   English (UK)           exception
982     //  fr    azerty F  Fr  Français  Français
983     //  fr_CA qwerty F  Fr  Français  Français (Canada)
984     //  de    qwertz F  De  Deutsch   Deutsch
985     //  zz    qwerty F      QWERTY    QWERTY
986     //  fr    qwertz T  Fr  Français  Français (QWERTZ)
987     //  de    qwerty T  De  Deutsch   Deutsch (QWERTY)
988     //  en_US azerty T  En  English   English (US) (AZERTY)
989     //  zz    azerty T      AZERTY    AZERTY
990 
991     // Get InputMethodSubtype's full display name in its locale.
getFullDisplayName(InputMethodSubtype subtype, Resources res)992     static String getFullDisplayName(InputMethodSubtype subtype, Resources res) {
993         if (SubtypeLocale.isNoLanguage(subtype)) {
994             return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
995         }
996 
997         return SubtypeLocale.getSubtypeDisplayName(subtype, res);
998     }
999 
1000     // Get InputMethodSubtype's short display name in its locale.
getShortDisplayName(InputMethodSubtype subtype)1001     static String getShortDisplayName(InputMethodSubtype subtype) {
1002         if (SubtypeLocale.isNoLanguage(subtype)) {
1003             return "";
1004         }
1005         final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
1006         return StringUtils.toTitleCase(locale.getLanguage(), locale);
1007     }
1008 
1009     // Get InputMethodSubtype's middle display name in its locale.
getMiddleDisplayName(InputMethodSubtype subtype)1010     static String getMiddleDisplayName(InputMethodSubtype subtype) {
1011         if (SubtypeLocale.isNoLanguage(subtype)) {
1012             return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
1013         }
1014         final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
1015         return StringUtils.toTitleCase(locale.getDisplayLanguage(locale), locale);
1016     }
1017 }
1018