1 /*
2  * Copyright (C) 2019 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.leanback;
18 
19 import android.animation.Animator;
20 import android.animation.ValueAnimator;
21 import android.speech.RecognitionListener;
22 import android.os.Bundle;
23 
24 import com.android.inputmethod.leanback.LeanbackKeyboardController.InputListener;
25 import com.android.inputmethod.leanback.voice.RecognizerView;
26 import com.android.inputmethod.leanback.voice.SpeechLevelSource;
27 import com.android.inputmethod.leanback.service.LeanbackImeService;
28 
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.res.Resources;
32 import android.view.KeyEvent;
33 import android.view.View;
34 import android.view.View.OnFocusChangeListener;
35 import android.view.ViewGroup;
36 import android.view.ViewGroup.LayoutParams;
37 import android.view.ViewGroup.MarginLayoutParams;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.view.accessibility.AccessibilityManager;
40 import android.view.animation.AccelerateInterpolator;
41 import android.animation.ValueAnimator.AnimatorUpdateListener;
42 import android.animation.Animator.AnimatorListener;
43 import android.view.animation.Animation;
44 import android.view.animation.DecelerateInterpolator;
45 import android.view.animation.Interpolator;
46 import android.view.animation.Transformation;
47 import android.view.inputmethod.EditorInfo;
48 import android.view.inputmethod.InputMethodManager;
49 import android.view.inputmethod.InputMethodSubtype;
50 import android.graphics.PointF;
51 import android.graphics.Rect;
52 import android.speech.RecognizerIntent;
53 import android.speech.SpeechRecognizer;
54 import android.text.TextUtils;
55 import android.text.method.QwertyKeyListener;
56 import android.text.style.LocaleSpan;
57 import android.widget.Button;
58 import android.widget.FrameLayout;
59 import android.widget.HorizontalScrollView;
60 import android.widget.LinearLayout;
61 import android.widget.RelativeLayout;
62 import android.widget.ScrollView;
63 import android.util.Log;
64 import android.inputmethodservice.Keyboard;
65 import android.inputmethodservice.Keyboard.Key;
66 
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.Locale;
70 
71 /**
72  * This is the keyboard container for GridIme that contains the following views:
73  * <ul>
74  * <li>voice button</li>
75  * <li>main keyboard</li>
76  * <li>action button</li>
77  * <li>focus bubble</li>
78  * <li>touch indicator</li>
79  * <li>candidate view</li>
80  * </ul>
81  * Keyboard grid layout:
82  *
83  * <pre>
84  * | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |OTH|   |
85  * |<- | - | - | - | - | - | - | - | - | ->|ER |ACT|
86  * |<- | - | - | M | A | I | N | - | - | ->|   |   |
87  * |<- | K | E | Y | B | O | A | R | D | ->|KEY|ION|
88  * |<- | - | - | - | - | - | - | - | - | ->|S  |   |
89  * </pre>
90  */
91 public class LeanbackKeyboardContainer {
92 
93     private static final String TAG = "LbKbContainer";
94     private static final boolean DEBUG = false;
95     private static final boolean VOICE_SUPPORTED = true;
96     private static final String IME_PRIVATE_OPTIONS_ESCAPE_NORTH_LEGACY = "EscapeNorth=1";
97     private static final String IME_PRIVATE_OPTIONS_ESCAPE_NORTH = "escapeNorth";
98     private static final String IME_PRIVATE_OPTIONS_VOICE_DISMISS_LEGACY = "VoiceDismiss=1";
99     private static final String IME_PRIVATE_OPTIONS_VOICE_DISMISS = "voiceDismiss";
100 
101     /**
102      * This is the length of animations that move an indicator across the keys. Snaps and flicks
103      * will use this duration for the movement.
104      */
105     private static final long MOVEMENT_ANIMATION_DURATION = 150;
106 
107     /**
108      * This interpolator is used for movement animations.
109      */
110     public static final Interpolator sMovementInterpolator = new DecelerateInterpolator(1.5f);
111 
112     /**
113      * These are the states that the view can be in and affect the icon appearance. NO_TOUCH is when
114      * there are no fingers down on the input device.
115      */
116     public static final int TOUCH_STATE_NO_TOUCH = 0;
117 
118     /**
119      * TOUCH_SNAP indicates that a finger is down but the indicator is still considered snapped to a
120      * letter. Once the user moves a given distance from the snapped position it will change to
121      * TOUCH_MOVE.
122      */
123     public static final int TOUCH_STATE_TOUCH_SNAP = 1;
124 
125     /**
126      * TOUCH_MOVE indicates the user is moving freely around the space and is not snapped to any
127      * letter.
128      */
129     public static final int TOUCH_STATE_TOUCH_MOVE = 2;
130 
131     /**
132      * CLICK indicates the selection button is currently pressed. When the button is released we
133      * will transition back to snap or no touch depending on whether there is still a finger down on
134      * the input device or not.
135      */
136     public static final int TOUCH_STATE_CLICK = 3;
137 
138     // The minimum distance the user must move their finger to transition from
139     // the SNAP to the MOVE state
140     public static final double TOUCH_MOVE_MIN_DISTANCE = .1;
141 
142     /**
143      * When processing a flick or dpad event it is easier to move a key width + a fudge factor than
144      * to directly compute what the next key position should be. This is the fudge factor.
145      */
146     public static final double DIRECTION_STEP_MULTIPLIER = 1.25;
147 
148     /**
149      * Directions sent to event listeners.
150      */
151     public static final int DIRECTION_LEFT = 1 << 0;
152     public static final int DIRECTION_DOWN = 1 << 1;
153     public static final int DIRECTION_RIGHT = 1 << 2;
154     public static final int DIRECTION_UP = 1 << 3;
155     public static final int DIRECTION_DOWN_LEFT = DIRECTION_DOWN | DIRECTION_LEFT;
156     public static final int DIRECTION_DOWN_RIGHT = DIRECTION_DOWN | DIRECTION_RIGHT;
157     public static final int DIRECTION_UP_RIGHT = DIRECTION_UP | DIRECTION_RIGHT;
158     public static final int DIRECTION_UP_LEFT = DIRECTION_UP | DIRECTION_LEFT;
159 
160     /**
161      * handler messages
162      */
163     // align selector in onStartInputView
164     private static final int MSG_START_INPUT_VIEW = 0;
165 
166     // If this were a physical keyboard the width in cm. This will be mapped
167     // to the width in pixels but is representative of the mapping from the
168     // remote input to the screen. Higher values will require larger moves to
169     // get across the keyboard
170     protected static final float PHYSICAL_WIDTH_CM = 12;
171     // If this were a physical keyboard the height in cm. This will be mapped
172     // to the height in pixels but is representative of the mapping from the
173     // remote input to the screen. Higher values will require larger moves to
174     // get across the keyboard
175     protected static final float PHYSICAL_HEIGHT_CM = 5;
176 
177     /**
178      * Listener for publishing voice input result to {@link LeanbackKeyboardController}
179      */
180     public static interface VoiceListener {
onVoiceResult(String result)181         public void onVoiceResult(String result);
182     }
183 
184     public static interface DismissListener {
onDismiss(boolean fromVoice)185         public void onDismiss(boolean fromVoice);
186     }
187 
188     /**
189      * Class for holding information about the currently focused key.
190      */
191     public static class KeyFocus {
192         public static final int TYPE_INVALID = -1;
193         public static final int TYPE_MAIN = 0;
194         public static final int TYPE_VOICE = 1;
195         public static final int TYPE_ACTION = 2;
196         public static final int TYPE_SUGGESTION = 3;
197 
198         /**
199          * The bounding box for the current focused key/view
200          */
201         final Rect rect;
202 
203         /**
204          * The index of the focused key or suggestion. This is invalid for views that don't have
205          * indexed items.
206          */
207         int index;
208 
209         /**
210          * The type of key which indicates which view/keyboard the focus is in.
211          */
212         int type;
213 
214         /**
215          * The key code for the focused key. This is invalid for views that don't use key codes.
216          */
217         int code;
218 
219         /**
220          * The text label for the focused key. This is invalid for views that don't use labels.
221          */
222         CharSequence label;
223 
KeyFocus()224         public KeyFocus() {
225             type = TYPE_INVALID;
226             rect = new Rect();
227         }
228 
229         @Override
toString()230         public String toString() {
231             StringBuilder bob = new StringBuilder();
232             bob.append("[type: ").append(type)
233                     .append(", index: ").append(index)
234                     .append(", code: ").append(code)
235                     .append(", label: ").append(label)
236                     .append(", rect: ").append(rect)
237                     .append("]");
238             return bob.toString();
239         }
240 
set(KeyFocus focus)241         public void set(KeyFocus focus) {
242             index = focus.index;
243             type = focus.type;
244             code = focus.code;
245             label = focus.label;
246             rect.set(focus.rect);
247         }
248 
249         @Override
equals(Object o)250         public boolean equals(Object o) {
251             if (this == o) {
252                 return true;
253             }
254             if (o == null || getClass() != o.getClass()) {
255                 return false;
256             }
257 
258             KeyFocus keyFocus = (KeyFocus) o;
259 
260             if (code != keyFocus.code) {
261                 return false;
262             }
263             if (index != keyFocus.index) {
264                 return false;
265             }
266             if (type != keyFocus.type) {
267                 return false;
268             }
269             if (label != null ? !label.equals(keyFocus.label) : keyFocus.label != null) {
270                 return false;
271             }
272             if (!rect.equals(keyFocus.rect)) {
273                 return false;
274             }
275 
276             return true;
277         }
278 
279         @Override
hashCode()280         public int hashCode() {
281             int result = rect.hashCode();
282             result = 31 * result + index;
283             result = 31 * result + type;
284             result = 31 * result + code;
285             result = 31 * result + (label != null ? label.hashCode() : 0);
286             return result;
287         }
288     }
289 
290     private class VoiceIntroAnimator {
291         private AnimatorListener mEnterListener;
292         private AnimatorListener mExitListener;
293         private ValueAnimator mValueAnimator;
294 
VoiceIntroAnimator(AnimatorListener enterListener, AnimatorListener exitListener)295         public VoiceIntroAnimator(AnimatorListener enterListener, AnimatorListener exitListener) {
296             mEnterListener = enterListener;
297             mExitListener = exitListener;
298 
299             mValueAnimator = ValueAnimator.ofFloat(mAlphaOut, mAlphaIn);
300             mValueAnimator.setDuration(mVoiceAnimDur);
301             mValueAnimator.setInterpolator(new AccelerateInterpolator());
302         }
303 
startEnterAnimation()304         void startEnterAnimation() {
305             if (!isVoiceVisible() && !mValueAnimator.isRunning()) {
306                 start(true);
307             }
308         }
309 
startExitAnimation()310         void startExitAnimation() {
311             if (isVoiceVisible() && !mValueAnimator.isRunning()) {
312                 start(false);
313             }
314         }
315 
start(final boolean enterVoice)316         private void start(final boolean enterVoice) {
317             // TODO make animation continous
318             mValueAnimator.cancel();
319 
320             mValueAnimator.removeAllListeners();
321             mValueAnimator.addListener(enterVoice ? mEnterListener : mExitListener);
322             mValueAnimator.removeAllUpdateListeners();
323             mValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
324 
325                 @Override
326                 public void onAnimationUpdate(ValueAnimator animation) {
327                     float progress = (Float) mValueAnimator.getAnimatedValue();
328                     float antiProgress = mAlphaIn + mAlphaOut - progress;
329 
330                     float kbAlpha = enterVoice ? antiProgress : progress;
331                     float voiceAlpha = enterVoice ? progress : antiProgress;
332 
333                     mMainKeyboardView.setAlpha(kbAlpha);
334                     mActionButtonView.setAlpha(kbAlpha);
335                     mVoiceButtonView.setAlpha(voiceAlpha);
336 
337                     if (progress == mAlphaOut) {
338                         // first pass
339                         if (enterVoice) {
340                             mVoiceButtonView.setVisibility(View.VISIBLE);
341                         } else {
342                             mMainKeyboardView.setVisibility(View.VISIBLE);
343                             mActionButtonView.setVisibility(View.VISIBLE);
344                         }
345                     } else if (progress == mAlphaIn) {
346                         // done
347                         if (enterVoice) {
348                             mMainKeyboardView.setVisibility(View.INVISIBLE);
349                             mActionButtonView.setVisibility(View.INVISIBLE);
350                         } else {
351                             mVoiceButtonView.setVisibility(View.INVISIBLE);
352                         }
353                     }
354                 }
355             });
356 
357             mValueAnimator.start();
358         }
359     }
360 
361     /**
362      * keyboard flags based on the edittext types
363      */
364     // if suggestions are enabled and suggestion view is visible
365     private boolean mSuggestionsEnabled;
366     // if auto entering space after period or suggestions is enabled
367     private boolean mAutoEnterSpaceEnabled;
368     // if voice button is enabled
369     private boolean mVoiceEnabled;
370     // initial main keyboard to show for the specific edittext
371     private Keyboard mInitialMainKeyboard;
372     // text resource id of the enter key. If set to 0, show enter key image
373     private int mEnterKeyTextResId;
374     private CharSequence mEnterKeyText;
375 
376     /**
377      * This animator controls the way the touch indicator grows and shrinks when changing states.
378      */
379     private ValueAnimator mSelectorAnimator;
380 
381     /**
382      * The current state of touch.
383      */
384     private int mTouchState = TOUCH_STATE_NO_TOUCH;
385 
386     private VoiceListener mVoiceListener;
387 
388     private DismissListener mDismissListener;
389 
390     private LeanbackImeService mContext;
391     private RelativeLayout mRootView;
392 
393     private View mKeyboardsContainer;
394     private View mSuggestionsBg;
395     private HorizontalScrollView mSuggestionsContainer;
396     private LinearLayout mSuggestions;
397     private LeanbackKeyboardView mMainKeyboardView;
398     private Button mActionButtonView;
399     private ScaleAnimation mSelectorAnimation;
400     private View mSelector;
401     private float mOverestimate;
402 
403     // The modeled physical position of the current selection in cm
404     private PointF mPhysicalSelectPos = new PointF(2, .5f);
405     // The position of the touch indicator in cm
406     private PointF mPhysicalTouchPos = new PointF(2, .5f);
407     // A point for doing temporary calculations
408     private PointF mTempPoint = new PointF();
409 
410     private KeyFocus mCurrKeyInfo = new KeyFocus();
411     private KeyFocus mDownKeyInfo = new KeyFocus();
412     private KeyFocus mTempKeyInfo = new KeyFocus();
413 
414     private LeanbackKeyboardView mPrevView;
415     private Rect mRect = new Rect();
416     private Float mX;
417     private Float mY;
418     private int mMiniKbKeyIndex;
419 
420     private final int mClickAnimDur;
421     private final int mVoiceAnimDur;
422     private final float mAlphaIn;
423     private final float mAlphaOut;
424 
425     private Keyboard mAbcKeyboard;
426     private Keyboard mSymKeyboard;
427     private Keyboard mNumKeyboard;
428 
429     // if we should capitalize the first letter in each sentence
430     private boolean mCapSentences;
431 
432     // if we should capitalize the first letter in each word
433     private boolean mCapWords;
434 
435     // if we should capitalize every character
436     private boolean mCapCharacters;
437 
438     // if voice is on
439     private boolean mVoiceOn;
440 
441     // Whether to allow escaping north or not
442     private boolean mEscapeNorthEnabled;
443 
444     // Whether to dismiss when voice button is pressed
445     private boolean mVoiceKeyDismissesEnabled;
446 
447     /**
448      * Voice
449      */
450     private Intent mRecognizerIntent;
451     private SpeechRecognizer mSpeechRecognizer;
452     private SpeechLevelSource mSpeechLevelSource;
453     private RecognizerView mVoiceButtonView;
454 
455     private class ScaleAnimation extends Animation {
456         private final ViewGroup.LayoutParams mParams;
457         private final View mView;
458         private float mStartX;
459         private float mStartY;
460         private float mStartWidth;
461         private float mStartHeight;
462         private float mEndX;
463         private float mEndY;
464         private float mEndWidth;
465         private float mEndHeight;
466 
ScaleAnimation(FrameLayout view)467         public ScaleAnimation(FrameLayout view) {
468             mView = view;
469             mParams = view.getLayoutParams();
470             setDuration(MOVEMENT_ANIMATION_DURATION);
471             setInterpolator(sMovementInterpolator);
472         }
473 
setAnimationBounds(float x, float y, float width, float height)474         public void setAnimationBounds(float x, float y, float width, float height) {
475             mEndX = x;
476             mEndY = y;
477             mEndWidth = width;
478             mEndHeight = height;
479         }
480 
481         @Override
applyTransformation(float interpolatedTime, Transformation t)482         protected void applyTransformation(float interpolatedTime, Transformation t) {
483             if (interpolatedTime == 0) {
484                 mStartX = mView.getX();
485                 mStartY = mView.getY();
486                 mStartWidth = mParams.width;
487                 mStartHeight = mParams.height;
488             } else {
489                 setValues(((mEndX - mStartX) * interpolatedTime + mStartX),
490                     ((mEndY - mStartY) * interpolatedTime + mStartY),
491                     ((int)((mEndWidth - mStartWidth) * interpolatedTime + mStartWidth)),
492                     ((int)((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight)));
493             }
494         }
495 
setValues(float x, float y, float width, float height)496         public void setValues(float x, float y, float width, float height) {
497             mView.setX(x);
498             mView.setY(y);
499             mParams.width = (int)(width);
500             mParams.height = (int)(height);
501             mView.setLayoutParams(mParams);
502             mView.requestLayout();
503         }
504     };
505 
506     private AnimatorListener mVoiceEnterListener = new AnimatorListener() {
507 
508         @Override
509         public void onAnimationStart(Animator animation) {
510             mSelector.setVisibility(View.INVISIBLE);
511             startRecognition(mContext);
512         }
513 
514         @Override
515         public void onAnimationRepeat(Animator animation) {
516         }
517 
518         @Override
519         public void onAnimationEnd(Animator animation) {
520         }
521 
522         @Override
523         public void onAnimationCancel(Animator animation) {
524         }
525     };
526 
527     private AnimatorListener mVoiceExitListener = new AnimatorListener() {
528 
529         @Override
530         public void onAnimationStart(Animator animation) {
531             mVoiceButtonView.showNotListening();
532             mSpeechRecognizer.cancel();
533             mSpeechRecognizer.setRecognitionListener(null);
534             mVoiceOn = false;
535         }
536 
537         @Override
538         public void onAnimationRepeat(Animator animation) {
539         }
540 
541         @Override
542         public void onAnimationEnd(Animator animation) {
543             mSelector.setVisibility(View.VISIBLE);
544         }
545 
546         @Override
547         public void onAnimationCancel(Animator animation) {
548         }
549     };
550 
551     private final VoiceIntroAnimator mVoiceAnimator;
552 
553     // Tracks whether or not a touch event is in progress. This is true while
554     // a finger is down on the pad.
555     private boolean mTouchDown = false;
556 
LeanbackKeyboardContainer(Context context)557     public LeanbackKeyboardContainer(Context context) {
558         mContext = (LeanbackImeService) context;
559 
560         final Resources res = mContext.getResources();
561         mVoiceAnimDur = res.getInteger(R.integer.voice_anim_duration);
562         mAlphaIn = res.getFraction(R.fraction.alpha_in, 1, 1);
563         mAlphaOut = res.getFraction(R.fraction.alpha_out, 1, 1);
564 
565         mVoiceAnimator = new VoiceIntroAnimator(mVoiceEnterListener, mVoiceExitListener);
566 
567         initKeyboards();
568 
569         mRootView = (RelativeLayout) mContext.getLayoutInflater()
570                 .inflate(R.layout.root_leanback, null);
571         mKeyboardsContainer = mRootView.findViewById(R.id.keyboard);
572         mSuggestionsBg = mRootView.findViewById(R.id.candidate_background);
573         mSuggestionsContainer =
574                 (HorizontalScrollView) mRootView.findViewById(R.id.suggestions_container);
575         mSuggestions = (LinearLayout) mSuggestionsContainer.findViewById(R.id.suggestions);
576 
577         mMainKeyboardView = (LeanbackKeyboardView) mRootView.findViewById(R.id.main_keyboard);
578         mVoiceButtonView = (RecognizerView) mRootView.findViewById(R.id.voice);
579 
580         mActionButtonView = (Button) mRootView.findViewById(R.id.enter);
581 
582         mSelector = mRootView.findViewById(R.id.selector);
583         mSelectorAnimation = new ScaleAnimation((FrameLayout) mSelector);
584 
585         mOverestimate = mContext.getResources().getFraction(R.fraction.focused_scale, 1, 1);
586         float scale = context.getResources().getFraction(R.fraction.clicked_scale, 1, 1);
587         mClickAnimDur = context.getResources().getInteger(R.integer.clicked_anim_duration);
588         mSelectorAnimator = ValueAnimator.ofFloat(1.0f, scale);
589         mSelectorAnimator.setDuration(mClickAnimDur);
590         mSelectorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
591             @Override
592             public void onAnimationUpdate(ValueAnimator animation) {
593                 float scale = (Float) animation.getAnimatedValue();
594                 mSelector.setScaleX(scale);
595                 mSelector.setScaleY(scale);
596             }
597         });
598 
599         mSpeechLevelSource = new SpeechLevelSource();
600         mVoiceButtonView.setSpeechLevelSource(mSpeechLevelSource);
601         mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(mContext);
602         mVoiceButtonView.setCallback(new RecognizerView.Callback() {
603             @Override
604             public void onStartRecordingClicked() {
605                 startVoiceRecording();
606             }
607 
608             @Override
609             public void onStopRecordingClicked() {
610                 cancelVoiceRecording();
611             }
612 
613             @Override
614             public void onCancelRecordingClicked() {
615                 cancelVoiceRecording();
616             }
617         });
618 
619     }
620 
startVoiceRecording()621     public void startVoiceRecording() {
622         if (mVoiceEnabled) {
623             if (mVoiceKeyDismissesEnabled) {
624                 if (DEBUG) Log.v(TAG, "Voice Dismiss");
625                 mDismissListener.onDismiss(true);
626             } else {
627                 mVoiceAnimator.startEnterAnimation();
628             }
629         }
630     }
631 
cancelVoiceRecording()632     public void cancelVoiceRecording() {
633         mVoiceAnimator.startExitAnimation();
634     }
635 
resetVoice()636     public void resetVoice() {
637         mMainKeyboardView.setAlpha(mAlphaIn);
638         mActionButtonView.setAlpha(mAlphaIn);
639         mVoiceButtonView.setAlpha(mAlphaOut);
640 
641         mMainKeyboardView.setVisibility(View.VISIBLE);
642         mActionButtonView.setVisibility(View.VISIBLE);
643         mVoiceButtonView.setVisibility(View.INVISIBLE);
644     }
645 
isVoiceVisible()646     public boolean isVoiceVisible() {
647         return mVoiceButtonView.getVisibility() == View.VISIBLE;
648     }
649 
initKeyboards()650     private void initKeyboards() {
651         Locale locale = Locale.getDefault();
652 
653         if (isMatch(locale, LeanbackLocales.QWERTY_GB)) {
654             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_en_gb);
655             mSymKeyboard = new Keyboard(mContext, R.xml.sym_en_gb);
656         } else if (isMatch(locale, LeanbackLocales.QWERTY_IN)) {
657             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_en_in);
658             mSymKeyboard = new Keyboard(mContext, R.xml.sym_en_in);
659         } else if (isMatch(locale, LeanbackLocales.QWERTY_ES_EU)) {
660             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_es_eu);
661             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
662         } else if (isMatch(locale, LeanbackLocales.QWERTY_ES_US)) {
663             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_es_us);
664             mSymKeyboard = new Keyboard(mContext, R.xml.sym_us);
665         } else if (isMatch(locale, LeanbackLocales.QWERTY_AZ)) {
666             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_az);
667             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
668         } else if (isMatch(locale, LeanbackLocales.QWERTY_CA)) {
669             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_ca);
670             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
671         } else if (isMatch(locale, LeanbackLocales.QWERTY_DA)) {
672             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_da);
673             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
674         } else if (isMatch(locale, LeanbackLocales.QWERTY_ET)) {
675             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_et);
676             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
677         } else if (isMatch(locale, LeanbackLocales.QWERTY_FI)) {
678             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_fi);
679             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
680         } else if (isMatch(locale, LeanbackLocales.QWERTY_NB)) {
681             // in the LatinIME nb uses the US symbols (usd instead of euro)
682             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_nb);
683             mSymKeyboard = new Keyboard(mContext, R.xml.sym_us);
684         } else if (isMatch(locale, LeanbackLocales.QWERTY_SV)) {
685             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_sv);
686             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
687         } else if (isMatch(locale, LeanbackLocales.QWERTY_US)) {
688             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_us);
689             mSymKeyboard = new Keyboard(mContext, R.xml.sym_us);
690         } else if (isMatch(locale, LeanbackLocales.QWERTZ_CH)) {
691             mAbcKeyboard = new Keyboard(mContext, R.xml.qwertz_ch);
692             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
693         } else if (isMatch(locale, LeanbackLocales.QWERTZ)) {
694             mAbcKeyboard = new Keyboard(mContext, R.xml.qwertz);
695             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
696         } else if (isMatch(locale, LeanbackLocales.AZERTY)) {
697             mAbcKeyboard = new Keyboard(mContext, R.xml.azerty);
698             mSymKeyboard = new Keyboard(mContext, R.xml.sym_azerty);
699         } else {
700             mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_eu);
701             mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
702         }
703 
704         mNumKeyboard = new Keyboard(mContext, R.xml.number);
705     }
706 
isMatch(Locale locale, Locale[] list)707     private boolean isMatch(Locale locale, Locale[] list) {
708         for (Locale compare : list) {
709             // comparison language is either blank or they match
710             if (TextUtils.isEmpty(compare.getLanguage()) ||
711                     TextUtils.equals(locale.getLanguage(), compare.getLanguage())) {
712                 // comparison country is either blank or they match
713                 if (TextUtils.isEmpty(compare.getCountry()) ||
714                             TextUtils.equals(locale.getCountry(), compare.getCountry())) {
715                     return true;
716                 }
717             }
718         }
719 
720         return false;
721     }
722 
723     /**
724      * This method is called when we start the input at a NEW input field to set up the IME options,
725      * such as suggestions, voice, and action
726      */
onStartInput(EditorInfo attribute)727     public void onStartInput(EditorInfo attribute) {
728         setImeOptions(mContext.getResources(), attribute);
729         mVoiceOn = false;
730     }
731 
732     /**
733      * This method is called whenever we bring up the IME at an input field.
734      */
onStartInputView()735     public void onStartInputView() {
736         // This must be done here because modifying the views before it is
737         // shown can cause selection handles to be shown if using a USB
738         // keyboard in a WebView.
739         clearSuggestions();
740 
741         RelativeLayout.LayoutParams lp =
742                 (RelativeLayout.LayoutParams) mKeyboardsContainer.getLayoutParams();
743         if (mSuggestionsEnabled) {
744             lp.removeRule(RelativeLayout.ALIGN_PARENT_TOP);
745             mSuggestionsContainer.setVisibility(View.VISIBLE);
746             mSuggestionsBg.setVisibility(View.VISIBLE);
747         } else {
748             lp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
749             mSuggestionsContainer.setVisibility(View.GONE);
750             mSuggestionsBg.setVisibility(View.GONE);
751         }
752         mKeyboardsContainer.setLayoutParams(lp);
753 
754         mMainKeyboardView.setKeyboard(mInitialMainKeyboard);
755         // TODO fix this for number keyboard
756         mVoiceButtonView.setMicEnabled(mVoiceEnabled);
757         resetVoice();
758         dismissMiniKeyboard();
759 
760         // setImeOptions will be called before this, setting the text resource value
761         if (!TextUtils.isEmpty(mEnterKeyText)) {
762             mActionButtonView.setText(mEnterKeyText);
763             mActionButtonView.setContentDescription(mEnterKeyText);
764         } else {
765             mActionButtonView.setText(mEnterKeyTextResId);
766             mActionButtonView.setContentDescription(mContext.getString(mEnterKeyTextResId));
767         }
768 
769         if (mCapCharacters) {
770             setShiftState(LeanbackKeyboardView.SHIFT_LOCKED);
771         } else if (mCapSentences || mCapWords) {
772             setShiftState(LeanbackKeyboardView.SHIFT_ON);
773         } else {
774             setShiftState(LeanbackKeyboardView.SHIFT_OFF);
775         }
776     }
777 
778     /**
779      * This method is called when the keyboard layout is complete, to set up the initial focus and
780      * visibility. This method gets called later than {@link onStartInput} and
781      * {@link onStartInputView}.
782      */
onInitInputView()783     public void onInitInputView() {
784         resetFocusCursor();
785         mSelector.setVisibility(View.VISIBLE);
786     }
787 
getView()788     public RelativeLayout getView() {
789         return mRootView;
790     }
791 
setVoiceListener(VoiceListener listener)792     public void setVoiceListener(VoiceListener listener) {
793         mVoiceListener = listener;
794     }
795 
setDismissListener(DismissListener listener)796     public void setDismissListener(DismissListener listener) {
797         mDismissListener = listener;
798     }
799 
setImeOptions(Resources resources, EditorInfo attribute)800     private void setImeOptions(Resources resources, EditorInfo attribute) {
801         mSuggestionsEnabled = true;
802         mAutoEnterSpaceEnabled = true;
803         mVoiceEnabled = true;
804         mInitialMainKeyboard = mAbcKeyboard;
805         mEscapeNorthEnabled = false;
806         mVoiceKeyDismissesEnabled = false;
807 
808         // set keyboard properties
809         switch (LeanbackUtils.getInputTypeClass(attribute)) {
810             case EditorInfo.TYPE_CLASS_NUMBER:
811             case EditorInfo.TYPE_CLASS_DATETIME:
812             case EditorInfo.TYPE_CLASS_PHONE:
813                 mSuggestionsEnabled = false;
814                 mVoiceEnabled = false;
815                 // TODO use number keyboard for these input types
816                 mInitialMainKeyboard = mAbcKeyboard;
817                 break;
818             case EditorInfo.TYPE_CLASS_TEXT:
819                 switch (LeanbackUtils.getInputTypeVariation(attribute)) {
820                     case EditorInfo.TYPE_TEXT_VARIATION_PASSWORD:
821                     case EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD:
822                     case EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:
823                     case EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME:
824                         mSuggestionsEnabled = false;
825                         mVoiceEnabled = false;
826                         mInitialMainKeyboard = mAbcKeyboard;
827                         break;
828                     case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
829                     case EditorInfo.TYPE_TEXT_VARIATION_URI:
830                     case EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT:
831                     case EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
832                         mSuggestionsEnabled = true;
833                         mAutoEnterSpaceEnabled = false;
834                         mVoiceEnabled = false;
835                         mInitialMainKeyboard = mAbcKeyboard;
836                         break;
837                 }
838                 break;
839         }
840 
841         if (mSuggestionsEnabled) {
842             mSuggestionsEnabled = (attribute.inputType
843                     & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) == 0;
844         }
845         if (mAutoEnterSpaceEnabled) {
846             mAutoEnterSpaceEnabled = mSuggestionsEnabled && mAutoEnterSpaceEnabled;
847         }
848         mCapSentences = (attribute.inputType
849                 & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0;
850         mCapWords = ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) ||
851                 (LeanbackUtils.getInputTypeVariation(attribute)
852                     == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME);
853         mCapCharacters = (attribute.inputType
854                 & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0;
855 
856         if (attribute.privateImeOptions != null) {
857             if (attribute.privateImeOptions.contains(IME_PRIVATE_OPTIONS_ESCAPE_NORTH) ||
858                     attribute.privateImeOptions.contains(
859                             IME_PRIVATE_OPTIONS_ESCAPE_NORTH_LEGACY)) {
860                 mEscapeNorthEnabled = true;
861             }
862             if (attribute.privateImeOptions.contains(IME_PRIVATE_OPTIONS_VOICE_DISMISS) ||
863                     attribute.privateImeOptions.contains(
864                             IME_PRIVATE_OPTIONS_VOICE_DISMISS_LEGACY)) {
865                 mVoiceKeyDismissesEnabled = true;
866             }
867         }
868 
869         if (DEBUG) {
870             Log.d(TAG, "sugg: " + mSuggestionsEnabled + " | capSentences: " + mCapSentences
871                     + " | capWords: " + mCapWords + " | capChar: " + mCapCharacters
872                     + " | escapeNorth: " + mEscapeNorthEnabled
873                     + " | voiceDismiss : " + mVoiceKeyDismissesEnabled
874             );
875         }
876 
877         // set enter key
878         mEnterKeyText = attribute.actionLabel;
879         if (TextUtils.isEmpty(mEnterKeyText)) {
880             switch (LeanbackUtils.getImeAction(attribute)) {
881                 case EditorInfo.IME_ACTION_GO:
882                     mEnterKeyTextResId = R.string.label_go_key;
883                     break;
884                 case EditorInfo.IME_ACTION_NEXT:
885                     mEnterKeyTextResId = R.string.label_next_key;
886                     break;
887                 case EditorInfo.IME_ACTION_SEARCH:
888                     mEnterKeyTextResId = R.string.label_search_key;
889                     break;
890                 case EditorInfo.IME_ACTION_SEND:
891                     mEnterKeyTextResId = R.string.label_send_key;
892                     break;
893                 default:
894                     mEnterKeyTextResId = R.string.label_done_key;
895                     break;
896             }
897         }
898 
899         if (!VOICE_SUPPORTED) {
900             mVoiceEnabled = false;
901         }
902     }
903 
isVoiceEnabled()904     public boolean isVoiceEnabled() {
905         return mVoiceEnabled;
906     }
907 
areSuggestionsEnabled()908     public boolean areSuggestionsEnabled() {
909         return mSuggestionsEnabled;
910     }
911 
enableAutoEnterSpace()912     public boolean enableAutoEnterSpace() {
913         return mAutoEnterSpaceEnabled;
914     }
915 
getAlignmentPosition(float posXCm, float posYCm, PointF result)916     private PointF getAlignmentPosition(float posXCm, float posYCm, PointF result) {
917         float width = mRootView.getWidth() - mRootView.getPaddingRight()
918                 - mRootView.getPaddingLeft()
919                 - mContext.getResources().getDimension(R.dimen.selector_size);
920         float height = mRootView.getHeight() - mRootView.getPaddingTop()
921                 - mRootView.getPaddingBottom()
922                 - mContext.getResources().getDimension(R.dimen.selector_size);
923         result.x = posXCm / PHYSICAL_WIDTH_CM * width + mRootView.getPaddingLeft();
924         result.y = posYCm / PHYSICAL_HEIGHT_CM * height + mRootView.getPaddingTop();
925         return result;
926     }
927 
getPhysicalPosition(float x, float y, PointF result)928     private void getPhysicalPosition(float x, float y, PointF result) {
929         x -= mSelector.getWidth() / 2;
930         y -= mSelector.getHeight() / 2;
931         float width = mRootView.getWidth() - mRootView.getPaddingRight()
932                 - mRootView.getPaddingLeft()
933                 - mContext.getResources().getDimension(R.dimen.selector_size);
934         float height = mRootView.getHeight() - mRootView.getPaddingTop()
935                 - mRootView.getPaddingBottom()
936                 - mContext.getResources().getDimension(R.dimen.selector_size);
937         float posXCm = (x - mRootView.getPaddingLeft()) * PHYSICAL_WIDTH_CM / width;
938         float posYCm = (y - mRootView.getPaddingTop()) * PHYSICAL_HEIGHT_CM / height;
939         result.x = posXCm;
940         result.y = posYCm;
941     }
942 
offsetRect(Rect rect, View view)943     private void offsetRect(Rect rect, View view) {
944         rect.left = 0;
945         rect.top = 0;
946         rect.right = view.getWidth();
947         rect.bottom = view.getHeight();
948         ((ViewGroup) mRootView).offsetDescendantRectToMyCoords(view, rect);
949     }
950 
951     /**
952      * Finds the {@link KeyFocus} on screen that best matches the given pixel positions
953      *
954      * @param x position in pixels, if null, use the last valid x value
955      * @param y position in pixels, if null, use the last valid y value
956      * @param focus the focus object to update with the result
957      * @return true if focus was successfully found, false otherwise.
958     */
getBestFocus(Float x, Float y, KeyFocus focus)959     public boolean getBestFocus(Float x, Float y, KeyFocus focus) {
960         boolean validFocus = true;
961 
962         offsetRect(mRect, mActionButtonView);
963         int actionLeft = mRect.left;
964 
965         offsetRect(mRect, mMainKeyboardView);
966         int keyboardTop = mRect.top;
967 
968         // use last if invalid
969         x = (x == null) ? mX : x;
970         y = (y == null) ? mY : y;
971 
972         final int count = mSuggestions.getChildCount();
973         if (y < keyboardTop && count > 0 && mSuggestionsEnabled) {
974             for (int i = 0; i < count; i++) {
975                 View suggestView = mSuggestions.getChildAt(i);
976                 offsetRect(mRect, suggestView);
977                 if (x < mRect.right || i+1 == count) {
978                     suggestView.requestFocus();
979                     LeanbackUtils.sendAccessibilityEvent(suggestView.findViewById(R.id.text), true);
980                     configureFocus(focus, mRect, i, KeyFocus.TYPE_SUGGESTION);
981                     break;
982                 }
983             }
984         } else if (y < keyboardTop && mEscapeNorthEnabled) {
985             validFocus = false;
986             escapeNorth();
987         } else if (x > actionLeft) {
988             // closest is the action button
989             offsetRect(mRect, mActionButtonView);
990             configureFocus(focus, mRect, 0, KeyFocus.TYPE_ACTION);
991         } else {
992             mX = x;
993             mY = y;
994 
995             // In the main view
996             offsetRect(mRect, mMainKeyboardView);
997             x = (x - mRect.left);
998             y = (y - mRect.top);
999 
1000             int index = mMainKeyboardView.getNearestIndex(x, y);
1001             Key key = mMainKeyboardView.getKey(index);
1002             configureFocus(focus, mRect, index, key, KeyFocus.TYPE_MAIN);
1003         }
1004 
1005         return validFocus;
1006     }
1007 
escapeNorth()1008     private void escapeNorth() {
1009         if (DEBUG) Log.v(TAG, "Escaping north");
1010         mDismissListener.onDismiss(false);
1011     }
1012 
configureFocus(KeyFocus focus, Rect rect, int index, int type)1013     private void configureFocus(KeyFocus focus, Rect rect, int index, int type) {
1014         focus.type = type;
1015         focus.index = index;
1016         focus.rect.set(rect);
1017     }
1018 
configureFocus(KeyFocus focus, Rect rect, int index, Key key, int type)1019     private void configureFocus(KeyFocus focus, Rect rect, int index, Key key, int type) {
1020         focus.type = type;
1021         if (key == null) {
1022             return;
1023         }
1024         if (key.codes != null) {
1025             focus.code = key.codes[0];
1026         } else {
1027             focus.code = KeyEvent.KEYCODE_UNKNOWN;
1028         }
1029         focus.index = index;
1030         focus.label = key.label;
1031         focus.rect.left = key.x + rect.left;
1032         focus.rect.top = key.y + rect.top;
1033         focus.rect.right = focus.rect.left + key.width;
1034         focus.rect.bottom = focus.rect.top + key.height;
1035     }
1036 
setKbFocus(KeyFocus focus, boolean forceFocusChange, boolean animate)1037     private void setKbFocus(KeyFocus focus, boolean forceFocusChange, boolean animate) {
1038         if (focus.equals(mCurrKeyInfo) && !forceFocusChange) {
1039             // Nothing changed
1040             return;
1041         }
1042         LeanbackKeyboardView prevView = mPrevView;
1043         mPrevView = null;
1044         boolean overestimateWidth = false;
1045         boolean overestimateHeight = false;
1046 
1047         switch (focus.type) {
1048             case KeyFocus.TYPE_VOICE:
1049                 mVoiceButtonView.setMicFocused(true);
1050                 dismissMiniKeyboard();
1051                 break;
1052             case KeyFocus.TYPE_ACTION:
1053                 LeanbackUtils.sendAccessibilityEvent(mActionButtonView, true);
1054                 dismissMiniKeyboard();
1055                 break;
1056             case KeyFocus.TYPE_SUGGESTION:
1057                 dismissMiniKeyboard();
1058                 break;
1059             case KeyFocus.TYPE_MAIN:
1060                 overestimateHeight = true;
1061                 overestimateWidth = (focus.code != LeanbackKeyboardView.ASCII_SPACE);
1062                 mMainKeyboardView.setFocus(focus.index, mTouchState == TOUCH_STATE_CLICK, overestimateWidth);
1063                 mPrevView = mMainKeyboardView;
1064                 break;
1065         }
1066 
1067         if (prevView != null && prevView != mPrevView) {
1068             prevView.setFocus(-1, mTouchState == TOUCH_STATE_CLICK);
1069         }
1070 
1071         setSelectorToFocus(focus.rect, overestimateWidth, overestimateHeight, animate);
1072         mCurrKeyInfo.set(focus);
1073     }
1074 
setSelectorToFocus(Rect rect, boolean overestimateWidth, boolean overestimateHeight, boolean animate)1075     public void setSelectorToFocus(Rect rect, boolean overestimateWidth, boolean overestimateHeight,
1076             boolean animate) {
1077         if (mSelector.getWidth() == 0 || mSelector.getHeight() == 0
1078                 || rect.width() == 0 || rect.height() == 0) {
1079             return;
1080         }
1081 
1082         float width = rect.width();
1083         float height = rect.height();
1084 
1085         if (overestimateHeight) {
1086             height *= mOverestimate;
1087         }
1088         if (overestimateWidth) {
1089             width *= mOverestimate;
1090         }
1091 
1092         float major = Math.max(width, height);
1093         float minor = Math.min(width, height);
1094         // if the difference between the width and height is less than 10%,
1095         // keep the width and height the same.
1096         if (major / minor < 1.1) {
1097             width = height = Math.max(width, height);
1098         }
1099 
1100         float x = rect.exactCenterX() - width/2;
1101         float y = rect.exactCenterY() - height/2;
1102         mSelectorAnimation.cancel();
1103         if (animate) {
1104             mSelectorAnimation.reset();
1105             mSelectorAnimation.setAnimationBounds(x, y, width, height);
1106             mSelector.startAnimation(mSelectorAnimation);
1107         } else {
1108             mSelectorAnimation.setValues(x, y, width, height);
1109         }
1110     }
1111 
getKey(int type, int index)1112     public Keyboard.Key getKey(int type, int index) {
1113         return (type == KeyFocus.TYPE_MAIN) ? mMainKeyboardView.getKey(index) : null;
1114     }
1115 
getCurrKeyCode()1116     public int getCurrKeyCode() {
1117         Key key = getKey(mCurrKeyInfo.type, mCurrKeyInfo.index);
1118         if (key != null) {
1119             return key.codes[0];
1120         }
1121         return 0;
1122     }
1123 
getTouchState()1124     public int getTouchState() {
1125         return mTouchState;
1126     }
1127 
1128     /**
1129      * Set the view state which affects how the touch indicator is drawn. This code currently
1130      * assumes the state changes below for simplicity. If the state machine is updated this code
1131      * should probably be checked to ensure it still works. NO_TOUCH -> on touch start -> SNAP SNAP
1132      * -> on enough movement -> MOVE MOVE -> on hover long enough -> SNAP SNAP -> on a click down ->
1133      * CLICK CLICK -> on click released -> SNAP ANY STATE -> on touch end -> NO_TOUCH
1134      *
1135      * @param state The new state to transition to
1136      */
setTouchState(int state)1137     public void setTouchState(int state) {
1138         switch (state) {
1139             case TOUCH_STATE_NO_TOUCH:
1140                 if (mTouchState == TOUCH_STATE_TOUCH_MOVE || mTouchState == TOUCH_STATE_CLICK) {
1141                     // If the touch indicator was small make it big again
1142                     mSelectorAnimator.reverse();
1143                 }
1144                 break;
1145             case TOUCH_STATE_TOUCH_SNAP:
1146                 if (mTouchState == TOUCH_STATE_CLICK) {
1147                     // And make the touch indicator big again
1148                     mSelectorAnimator.reverse();
1149                 } else if (mTouchState == TOUCH_STATE_TOUCH_MOVE) {
1150                     // Just make the touch indicator big
1151                     mSelectorAnimator.reverse();
1152                 }
1153                 break;
1154             case TOUCH_STATE_TOUCH_MOVE:
1155                 if (mTouchState == TOUCH_STATE_NO_TOUCH || mTouchState == TOUCH_STATE_TOUCH_SNAP) {
1156                     // Shrink the touch indicator
1157                     mSelectorAnimator.start();
1158                 }
1159                 break;
1160             case TOUCH_STATE_CLICK:
1161                 if (mTouchState == TOUCH_STATE_NO_TOUCH || mTouchState == TOUCH_STATE_TOUCH_SNAP) {
1162                     // Shrink the touch indicator
1163                     mSelectorAnimator.start();
1164                 }
1165                 break;
1166         }
1167         setTouchStateInternal(state);
1168         setKbFocus(mCurrKeyInfo, true, true);
1169     }
1170 
getCurrFocus()1171     public KeyFocus getCurrFocus() {
1172         return mCurrKeyInfo;
1173     }
1174 
onVoiceClick()1175     public void onVoiceClick() {
1176         if (mVoiceButtonView != null) {
1177             mVoiceButtonView.onClick();
1178         }
1179     }
1180 
onModeChangeClick()1181     public void onModeChangeClick() {
1182         dismissMiniKeyboard();
1183         if (mMainKeyboardView.getKeyboard().equals(mSymKeyboard)) {
1184             mMainKeyboardView.setKeyboard(mAbcKeyboard);
1185         } else {
1186             mMainKeyboardView.setKeyboard(mSymKeyboard);
1187         }
1188     }
1189 
onShiftClick()1190     public void onShiftClick() {
1191         setShiftState(mMainKeyboardView.isShifted() ? LeanbackKeyboardView.SHIFT_OFF
1192                 : LeanbackKeyboardView.SHIFT_ON);
1193     }
1194 
onTextEntry()1195     public void onTextEntry() {
1196         // reset shift if caps is not on
1197         if (mMainKeyboardView.isShifted()) {
1198             if (!isCapsLockOn() && !mCapCharacters) {
1199                 setShiftState(LeanbackKeyboardView.SHIFT_OFF);
1200             }
1201         } else {
1202             if (isCapsLockOn() || mCapCharacters) {
1203                 setShiftState(LeanbackKeyboardView.SHIFT_LOCKED);
1204             }
1205         }
1206 
1207         if (dismissMiniKeyboard()) {
1208             moveFocusToIndex(mMiniKbKeyIndex, KeyFocus.TYPE_MAIN);
1209         }
1210     }
1211 
onSpaceEntry()1212     public void onSpaceEntry() {
1213         if (mMainKeyboardView.isShifted()) {
1214             if (!isCapsLockOn() && !mCapCharacters && !mCapWords) {
1215                 setShiftState(LeanbackKeyboardView.SHIFT_OFF);
1216             }
1217         } else {
1218             if (isCapsLockOn() || mCapCharacters || mCapWords) {
1219                 setShiftState(LeanbackKeyboardView.SHIFT_ON);
1220             }
1221         }
1222     }
1223 
onPeriodEntry()1224     public void onPeriodEntry() {
1225         if (mMainKeyboardView.isShifted()) {
1226             if (!isCapsLockOn() && !mCapCharacters && !mCapWords && !mCapSentences) {
1227                 setShiftState(LeanbackKeyboardView.SHIFT_OFF);
1228             }
1229         } else {
1230             if (isCapsLockOn() || mCapCharacters || mCapWords || mCapSentences) {
1231                 setShiftState(LeanbackKeyboardView.SHIFT_ON);
1232             }
1233         }
1234     }
1235 
dismissMiniKeyboard()1236     public boolean dismissMiniKeyboard() {
1237         return mMainKeyboardView.dismissMiniKeyboard();
1238     }
1239 
isCurrKeyShifted()1240     public boolean isCurrKeyShifted() {
1241         return mMainKeyboardView.isShifted();
1242     }
1243 
getSuggestionText(int index)1244     public CharSequence getSuggestionText(int index) {
1245         CharSequence text = null;
1246 
1247         if(index >= 0 && index < mSuggestions.getChildCount()){
1248             Button suggestion =
1249                     (Button) mSuggestions.getChildAt(index).findViewById(R.id.text);
1250             if (suggestion != null) {
1251                 text = suggestion.getText();
1252             }
1253         }
1254 
1255         return text;
1256     }
1257 
1258     /**
1259      * This method sets the keyboard focus and update the layout of the new focus
1260      *
1261      * @param focus the new focus of the keyboard
1262      */
setFocus(KeyFocus focus)1263     public void setFocus(KeyFocus focus) {
1264         setKbFocus(focus, false, true);
1265     }
1266 
getNextFocusInDirection(int direction, KeyFocus startFocus, KeyFocus nextFocus)1267     public boolean getNextFocusInDirection(int direction, KeyFocus startFocus, KeyFocus nextFocus) {
1268         boolean validNextFocus = true;
1269 
1270         switch (startFocus.type) {
1271             case KeyFocus.TYPE_VOICE:
1272                 // TODO move between voice button and kb button
1273                 break;
1274             case KeyFocus.TYPE_ACTION:
1275                 offsetRect(mRect, mMainKeyboardView);
1276                 if ((direction & DIRECTION_LEFT) != 0) {
1277                     // y is null, so we use the last y.  This way a user can hold left and wrap
1278                     // around the keyboard while staying in the same row
1279                     validNextFocus = getBestFocus((float) mRect.right, null, nextFocus);
1280                 } else if ((direction & DIRECTION_UP) != 0) {
1281                     offsetRect(mRect, mSuggestions);
1282                     validNextFocus = getBestFocus(
1283                             (float) startFocus.rect.centerX(), (float) mRect.centerY(), nextFocus);
1284                 }
1285                 break;
1286             case KeyFocus.TYPE_SUGGESTION:
1287                 if ((direction & DIRECTION_DOWN) != 0) {
1288                     offsetRect(mRect, mMainKeyboardView);
1289                     validNextFocus = getBestFocus(
1290                             (float) startFocus.rect.centerX(), (float) mRect.top, nextFocus);
1291                 } else if ((direction & DIRECTION_UP) != 0) {
1292                     if (mEscapeNorthEnabled) {
1293                         escapeNorth();
1294                     }
1295                 } else {
1296                     boolean left = (direction & DIRECTION_LEFT) != 0;
1297                     boolean right = (direction & DIRECTION_RIGHT) != 0;
1298 
1299                     if (left || right) {
1300                         // Cannot offset on the suggestion container because as it scrolls those
1301                         // values change
1302                         offsetRect(mRect, mRootView);
1303                         MarginLayoutParams lp =
1304                                 (MarginLayoutParams) mSuggestionsContainer.getLayoutParams();
1305                         int leftSide = mRect.left + lp.leftMargin;
1306                         int rightSide = mRect.right - lp.rightMargin;
1307                         int index = startFocus.index + (left ? -1 : 1);
1308 
1309                         View suggestView = mSuggestions.getChildAt(index);
1310                         if (suggestView != null) {
1311                             offsetRect(mRect, suggestView);
1312 
1313                             if (mRect.left < leftSide && mRect.right > rightSide) {
1314                                 mRect.left = leftSide;
1315                                 mRect.right = rightSide;
1316                             } else if (mRect.left < leftSide) {
1317                                 mRect.right = leftSide + mRect.width();
1318                                 mRect.left = leftSide;
1319                             } else if (mRect.right > rightSide) {
1320                                 mRect.left = rightSide - mRect.width();
1321                                 mRect.right = rightSide;
1322                             }
1323 
1324                             suggestView.requestFocus();
1325                             LeanbackUtils.sendAccessibilityEvent(
1326                                     suggestView.findViewById(R.id.text), true);
1327                             configureFocus(nextFocus, mRect, index, KeyFocus.TYPE_SUGGESTION);
1328                         }
1329                     }
1330                 }
1331                 break;
1332             case KeyFocus.TYPE_MAIN:
1333                 Key key = getKey(startFocus.type, startFocus.index);
1334                 // Step within the view.  Using height because all keys are the same height
1335                 // and widths vary.  Half the height is to ensure the next key is reached
1336                 float extraSlide = startFocus.rect.height()/2.0f;
1337                 float x = startFocus.rect.centerX();
1338                 float y = startFocus.rect.centerY();
1339                 if (startFocus.code == LeanbackKeyboardView.ASCII_SPACE) {
1340                     // if we're moving off of space, use the old x position for memory
1341                     x = mX;
1342                 }
1343                 if ((direction & DIRECTION_LEFT) != 0) {
1344                     if ((key.edgeFlags & Keyboard.EDGE_LEFT) == 0) {
1345                         // not on the left edge of the kb
1346                         x = startFocus.rect.left - extraSlide;
1347                     }
1348                 } else if ((direction & DIRECTION_RIGHT) != 0) {
1349                     if ((key.edgeFlags & Keyboard.EDGE_RIGHT) != 0) {
1350                         // jump to the action button
1351                         offsetRect(mRect, mActionButtonView);
1352                         x = mRect.centerX();
1353                     } else {
1354                         x = startFocus.rect.right + extraSlide;
1355                     }
1356                 }
1357                 // Don't need any special handling for up/down due to
1358                 // layout positioning. If the layout changes this should be
1359                 // reconsidered.
1360                 if ((direction & DIRECTION_UP) != 0) {
1361                     y -= startFocus.rect.height() * DIRECTION_STEP_MULTIPLIER;
1362                 } else if ((direction & DIRECTION_DOWN) != 0) {
1363                     y += startFocus.rect.height() * DIRECTION_STEP_MULTIPLIER;
1364                 }
1365                 getPhysicalPosition(x, y, mTempPoint);
1366                 validNextFocus = getBestFocus(x, y, nextFocus);
1367                 break;
1368         }
1369 
1370         return validNextFocus;
1371     }
1372 
getTouchSnapPosition()1373     private PointF getTouchSnapPosition() {
1374         PointF snapPos = new PointF();
1375         getPhysicalPosition(mCurrKeyInfo.rect.centerX(), mCurrKeyInfo.rect.centerY(), snapPos);
1376         return snapPos;
1377     }
1378 
clearSuggestions()1379     public void clearSuggestions() {
1380         mSuggestions.removeAllViews();
1381 
1382         if (getCurrFocus().type == KeyFocus.TYPE_SUGGESTION) {
1383             resetFocusCursor();
1384         }
1385     }
1386 
updateSuggestions(ArrayList<String> suggestions)1387     public void updateSuggestions(ArrayList<String> suggestions) {
1388         final int oldCount = mSuggestions.getChildCount();
1389         final int newCount = suggestions.size();
1390 
1391         if (newCount < oldCount) {
1392             // remove excess views
1393             mSuggestions.removeViews(newCount, oldCount-newCount);
1394         } else if (newCount > oldCount) {
1395             // add more
1396             for (int i = oldCount; i < newCount; i++) {
1397                 View suggestion =  mContext.getLayoutInflater()
1398                         .inflate(R.layout.candidate, null);
1399                 mSuggestions.addView(suggestion);
1400             }
1401         }
1402 
1403         for (int i = 0; i < newCount; i++) {
1404             Button suggestion =
1405                     (Button) mSuggestions.getChildAt(i).findViewById(R.id.text);
1406             suggestion.setText(suggestions.get(i));
1407             suggestion.setContentDescription(suggestions.get(i));
1408         }
1409 
1410         if (getCurrFocus().type == KeyFocus.TYPE_SUGGESTION) {
1411             resetFocusCursor();
1412         }
1413     }
1414 
1415     /**
1416      * Moves the selector back to the entry point key (T in general)
1417      */
resetFocusCursor()1418     public void resetFocusCursor() {
1419         // T is the best starting letter, it's in the 5th column and 2nd row,
1420         // this approximates that location
1421         double x = 0.45;
1422         double y = 0.375;
1423         offsetRect(mRect, mMainKeyboardView);
1424         mX = (float)(mRect.left + x*mRect.width());
1425         mY = (float)(mRect.top + y*mRect.height());
1426         getBestFocus(mX, mY, mTempKeyInfo);
1427         setKbFocus(mTempKeyInfo, true, false);
1428 
1429         setTouchStateInternal(TOUCH_STATE_NO_TOUCH);
1430         mSelectorAnimator.reverse();
1431         mSelectorAnimator.end();
1432     }
1433 
setTouchStateInternal(int state)1434     private void setTouchStateInternal(int state) {
1435         mTouchState = state;
1436     }
1437 
setShiftState(int state)1438     private void setShiftState(int state) {
1439         mMainKeyboardView.setShiftState(state);
1440     }
1441 
startRecognition(Context context)1442     private void startRecognition(Context context) {
1443         mRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
1444         mRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
1445                 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
1446         mRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
1447         mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
1448             float peakRmsLevel = 0;
1449             int rmsCounter = 0;
1450 
1451             @Override
1452             public void onBeginningOfSpeech() {
1453                 mVoiceButtonView.showRecording();
1454             }
1455 
1456             @Override
1457             public void onEndOfSpeech() {
1458                 mVoiceButtonView.showRecognizing();
1459                 mVoiceOn = false;
1460             }
1461 
1462             @Override
1463             public void onError(int error) {
1464                 cancelVoiceRecording();
1465                 switch (error) {
1466                     case SpeechRecognizer.ERROR_NO_MATCH:
1467                         Log.d(TAG, "recognizer error no match");
1468                         break;
1469                     case SpeechRecognizer.ERROR_SERVER:
1470                         Log.d(TAG, "recognizer error server error");
1471                         break;
1472                     case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
1473                         Log.d(TAG, "recognizer error speech timeout");
1474                         break;
1475                     case SpeechRecognizer.ERROR_CLIENT:
1476                         Log.d(TAG, "recognizer error client error");
1477                         break;
1478                     default:
1479                         Log.d(TAG, "recognizer other error " + error);
1480                         break;
1481                 }
1482             }
1483 
1484             @Override
1485             public synchronized void onPartialResults(Bundle partialResults) {
1486             }
1487 
1488             @Override
1489             public void onReadyForSpeech(Bundle params) {
1490                 mVoiceButtonView.showListening();
1491             }
1492 
1493             @Override
1494             public void onEvent(int eventType, Bundle params) {
1495             }
1496 
1497             @Override
1498             public void onBufferReceived(byte[] buffer) {
1499             }
1500 
1501             @Override
1502             public synchronized void onRmsChanged(float rmsdB) {
1503 
1504                 mVoiceOn = true;
1505                 mSpeechLevelSource.setSpeechLevel((rmsdB < 0) ? 0 : (int) (10 * rmsdB));
1506                 peakRmsLevel = Math.max(rmsdB, peakRmsLevel);
1507                 rmsCounter++;
1508 
1509                 if (rmsCounter > 100 && peakRmsLevel == 0) {
1510                     mVoiceButtonView.showNotListening();
1511                 }
1512             }
1513 
1514             @Override
1515             public void onResults(Bundle results) {
1516                 final ArrayList<String> matches =
1517                         results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
1518                 if (matches != null) {
1519                     if (mVoiceListener != null) {
1520                         mVoiceListener.onVoiceResult(matches.get(0));
1521                     }
1522                 }
1523 
1524                 cancelVoiceRecording();
1525             }
1526         });
1527         mSpeechRecognizer.startListening(mRecognizerIntent);
1528     }
1529 
isMiniKeyboardOnScreen()1530     public boolean isMiniKeyboardOnScreen() {
1531         return mMainKeyboardView.isMiniKeyboardOnScreen();
1532     }
1533 
onKeyLongPress()1534     public boolean onKeyLongPress() {
1535         if (mCurrKeyInfo.code == Keyboard.KEYCODE_SHIFT) {
1536             onToggleCapsLock();
1537             setTouchState(TOUCH_STATE_NO_TOUCH);
1538             return true;
1539         } else if (mCurrKeyInfo.type == KeyFocus.TYPE_MAIN) {
1540             mMainKeyboardView.onKeyLongPress();
1541             if (mMainKeyboardView.isMiniKeyboardOnScreen()) {
1542                 mMiniKbKeyIndex = mCurrKeyInfo.index;
1543                 moveFocusToIndex(mMainKeyboardView.getBaseMiniKbIndex(), KeyFocus.TYPE_MAIN);
1544                 return true;
1545             }
1546         }
1547 
1548         return false;
1549     }
1550 
moveFocusToIndex(int index, int type)1551     private void moveFocusToIndex(int index, int type) {
1552         Key key = mMainKeyboardView.getKey(index);
1553         configureFocus(mTempKeyInfo, mRect, index, key, type);
1554         setTouchState(TOUCH_STATE_NO_TOUCH);
1555         setKbFocus(mTempKeyInfo, true, true);
1556     }
1557 
onToggleCapsLock()1558     private void onToggleCapsLock() {
1559         onShiftDoubleClick(isCapsLockOn());
1560     }
1561 
onShiftDoubleClick(boolean wasCapsLockOn)1562     public void onShiftDoubleClick(boolean wasCapsLockOn) {
1563         setShiftState(
1564                 wasCapsLockOn ? LeanbackKeyboardView.SHIFT_OFF : LeanbackKeyboardView.SHIFT_LOCKED);
1565     }
1566 
isCapsLockOn()1567     public boolean isCapsLockOn() {
1568         return mMainKeyboardView.getShiftState() == LeanbackKeyboardView.SHIFT_LOCKED;
1569     }
1570 }
1571