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