blob: 124fdf362bd78bf96bafc6292da76bf9130d0f97 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.inputmethod.leanback;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.speech.RecognitionListener;
import android.os.Bundle;
import com.android.inputmethod.leanback.LeanbackKeyboardController.InputListener;
import com.android.inputmethod.leanback.voice.RecognizerView;
import com.android.inputmethod.leanback.voice.SpeechLevelSource;
import com.android.inputmethod.leanback.service.LeanbackImeService;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateInterpolator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.animation.Animator.AnimatorListener;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.graphics.PointF;
import android.graphics.Rect;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.text.TextUtils;
import android.text.method.QwertyKeyListener;
import android.text.style.LocaleSpan;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.util.Log;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.Keyboard.Key;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* This is the keyboard container for GridIme that contains the following views:
* <ul>
* <li>voice button</li>
* <li>main keyboard</li>
* <li>action button</li>
* <li>focus bubble</li>
* <li>touch indicator</li>
* <li>candidate view</li>
* </ul>
* Keyboard grid layout:
*
* <pre>
* | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |OTH| |
* |<- | - | - | - | - | - | - | - | - | ->|ER |ACT|
* |<- | - | - | M | A | I | N | - | - | ->| | |
* |<- | K | E | Y | B | O | A | R | D | ->|KEY|ION|
* |<- | - | - | - | - | - | - | - | - | ->|S | |
* </pre>
*/
public class LeanbackKeyboardContainer {
private static final String TAG = "LbKbContainer";
private static final boolean DEBUG = false;
private static final boolean VOICE_SUPPORTED = true;
private static final String IME_PRIVATE_OPTIONS_ESCAPE_NORTH_LEGACY = "EscapeNorth=1";
private static final String IME_PRIVATE_OPTIONS_ESCAPE_NORTH = "escapeNorth";
private static final String IME_PRIVATE_OPTIONS_VOICE_DISMISS_LEGACY = "VoiceDismiss=1";
private static final String IME_PRIVATE_OPTIONS_VOICE_DISMISS = "voiceDismiss";
/**
* This is the length of animations that move an indicator across the keys. Snaps and flicks
* will use this duration for the movement.
*/
private static final long MOVEMENT_ANIMATION_DURATION = 150;
/**
* This interpolator is used for movement animations.
*/
public static final Interpolator sMovementInterpolator = new DecelerateInterpolator(1.5f);
/**
* These are the states that the view can be in and affect the icon appearance. NO_TOUCH is when
* there are no fingers down on the input device.
*/
public static final int TOUCH_STATE_NO_TOUCH = 0;
/**
* TOUCH_SNAP indicates that a finger is down but the indicator is still considered snapped to a
* letter. Once the user moves a given distance from the snapped position it will change to
* TOUCH_MOVE.
*/
public static final int TOUCH_STATE_TOUCH_SNAP = 1;
/**
* TOUCH_MOVE indicates the user is moving freely around the space and is not snapped to any
* letter.
*/
public static final int TOUCH_STATE_TOUCH_MOVE = 2;
/**
* CLICK indicates the selection button is currently pressed. When the button is released we
* will transition back to snap or no touch depending on whether there is still a finger down on
* the input device or not.
*/
public static final int TOUCH_STATE_CLICK = 3;
// The minimum distance the user must move their finger to transition from
// the SNAP to the MOVE state
public static final double TOUCH_MOVE_MIN_DISTANCE = .1;
/**
* When processing a flick or dpad event it is easier to move a key width + a fudge factor than
* to directly compute what the next key position should be. This is the fudge factor.
*/
public static final double DIRECTION_STEP_MULTIPLIER = 1.25;
/**
* Directions sent to event listeners.
*/
public static final int DIRECTION_LEFT = 1 << 0;
public static final int DIRECTION_DOWN = 1 << 1;
public static final int DIRECTION_RIGHT = 1 << 2;
public static final int DIRECTION_UP = 1 << 3;
public static final int DIRECTION_DOWN_LEFT = DIRECTION_DOWN | DIRECTION_LEFT;
public static final int DIRECTION_DOWN_RIGHT = DIRECTION_DOWN | DIRECTION_RIGHT;
public static final int DIRECTION_UP_RIGHT = DIRECTION_UP | DIRECTION_RIGHT;
public static final int DIRECTION_UP_LEFT = DIRECTION_UP | DIRECTION_LEFT;
/**
* handler messages
*/
// align selector in onStartInputView
private static final int MSG_START_INPUT_VIEW = 0;
// If this were a physical keyboard the width in cm. This will be mapped
// to the width in pixels but is representative of the mapping from the
// remote input to the screen. Higher values will require larger moves to
// get across the keyboard
protected static final float PHYSICAL_WIDTH_CM = 12;
// If this were a physical keyboard the height in cm. This will be mapped
// to the height in pixels but is representative of the mapping from the
// remote input to the screen. Higher values will require larger moves to
// get across the keyboard
protected static final float PHYSICAL_HEIGHT_CM = 5;
/**
* Listener for publishing voice input result to {@link LeanbackKeyboardController}
*/
public static interface VoiceListener {
public void onVoiceResult(String result);
}
public static interface DismissListener {
public void onDismiss(boolean fromVoice);
}
/**
* Class for holding information about the currently focused key.
*/
public static class KeyFocus {
public static final int TYPE_INVALID = -1;
public static final int TYPE_MAIN = 0;
public static final int TYPE_VOICE = 1;
public static final int TYPE_ACTION = 2;
public static final int TYPE_SUGGESTION = 3;
/**
* The bounding box for the current focused key/view
*/
final Rect rect;
/**
* The index of the focused key or suggestion. This is invalid for views that don't have
* indexed items.
*/
int index;
/**
* The type of key which indicates which view/keyboard the focus is in.
*/
int type;
/**
* The key code for the focused key. This is invalid for views that don't use key codes.
*/
int code;
/**
* The text label for the focused key. This is invalid for views that don't use labels.
*/
CharSequence label;
public KeyFocus() {
type = TYPE_INVALID;
rect = new Rect();
}
@Override
public String toString() {
StringBuilder bob = new StringBuilder();
bob.append("[type: ").append(type)
.append(", index: ").append(index)
.append(", code: ").append(code)
.append(", label: ").append(label)
.append(", rect: ").append(rect)
.append("]");
return bob.toString();
}
public void set(KeyFocus focus) {
index = focus.index;
type = focus.type;
code = focus.code;
label = focus.label;
rect.set(focus.rect);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
KeyFocus keyFocus = (KeyFocus) o;
if (code != keyFocus.code) {
return false;
}
if (index != keyFocus.index) {
return false;
}
if (type != keyFocus.type) {
return false;
}
if (label != null ? !label.equals(keyFocus.label) : keyFocus.label != null) {
return false;
}
if (!rect.equals(keyFocus.rect)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = rect.hashCode();
result = 31 * result + index;
result = 31 * result + type;
result = 31 * result + code;
result = 31 * result + (label != null ? label.hashCode() : 0);
return result;
}
}
private class VoiceIntroAnimator {
private AnimatorListener mEnterListener;
private AnimatorListener mExitListener;
private ValueAnimator mValueAnimator;
public VoiceIntroAnimator(AnimatorListener enterListener, AnimatorListener exitListener) {
mEnterListener = enterListener;
mExitListener = exitListener;
mValueAnimator = ValueAnimator.ofFloat(mAlphaOut, mAlphaIn);
mValueAnimator.setDuration(mVoiceAnimDur);
mValueAnimator.setInterpolator(new AccelerateInterpolator());
}
void startEnterAnimation() {
if (!isVoiceVisible() && !mValueAnimator.isRunning()) {
start(true);
}
}
void startExitAnimation() {
if (isVoiceVisible() && !mValueAnimator.isRunning()) {
start(false);
}
}
private void start(final boolean enterVoice) {
// TODO make animation continous
mValueAnimator.cancel();
mValueAnimator.removeAllListeners();
mValueAnimator.addListener(enterVoice ? mEnterListener : mExitListener);
mValueAnimator.removeAllUpdateListeners();
mValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (Float) mValueAnimator.getAnimatedValue();
float antiProgress = mAlphaIn + mAlphaOut - progress;
float kbAlpha = enterVoice ? antiProgress : progress;
float voiceAlpha = enterVoice ? progress : antiProgress;
mMainKeyboardView.setAlpha(kbAlpha);
mActionButtonView.setAlpha(kbAlpha);
mVoiceButtonView.setAlpha(voiceAlpha);
if (progress == mAlphaOut) {
// first pass
if (enterVoice) {
mVoiceButtonView.setVisibility(View.VISIBLE);
} else {
mMainKeyboardView.setVisibility(View.VISIBLE);
mActionButtonView.setVisibility(View.VISIBLE);
}
} else if (progress == mAlphaIn) {
// done
if (enterVoice) {
mMainKeyboardView.setVisibility(View.INVISIBLE);
mActionButtonView.setVisibility(View.INVISIBLE);
} else {
mVoiceButtonView.setVisibility(View.INVISIBLE);
}
}
}
});
mValueAnimator.start();
}
}
/**
* keyboard flags based on the edittext types
*/
// if suggestions are enabled and suggestion view is visible
private boolean mSuggestionsEnabled;
// if auto entering space after period or suggestions is enabled
private boolean mAutoEnterSpaceEnabled;
// if voice button is enabled
private boolean mVoiceEnabled;
// initial main keyboard to show for the specific edittext
private Keyboard mInitialMainKeyboard;
// text resource id of the enter key. If set to 0, show enter key image
private int mEnterKeyTextResId;
private CharSequence mEnterKeyText;
/**
* This animator controls the way the touch indicator grows and shrinks when changing states.
*/
private ValueAnimator mSelectorAnimator;
/**
* The current state of touch.
*/
private int mTouchState = TOUCH_STATE_NO_TOUCH;
private VoiceListener mVoiceListener;
private DismissListener mDismissListener;
private LeanbackImeService mContext;
private RelativeLayout mRootView;
private View mKeyboardsContainer;
private View mSuggestionsBg;
private HorizontalScrollView mSuggestionsContainer;
private LinearLayout mSuggestions;
private LeanbackKeyboardView mMainKeyboardView;
private Button mActionButtonView;
private ScaleAnimation mSelectorAnimation;
private View mSelector;
private float mOverestimate;
// The modeled physical position of the current selection in cm
private PointF mPhysicalSelectPos = new PointF(2, .5f);
// The position of the touch indicator in cm
private PointF mPhysicalTouchPos = new PointF(2, .5f);
// A point for doing temporary calculations
private PointF mTempPoint = new PointF();
private KeyFocus mCurrKeyInfo = new KeyFocus();
private KeyFocus mDownKeyInfo = new KeyFocus();
private KeyFocus mTempKeyInfo = new KeyFocus();
private LeanbackKeyboardView mPrevView;
private Rect mRect = new Rect();
private Float mX;
private Float mY;
private int mMiniKbKeyIndex;
private final int mClickAnimDur;
private final int mVoiceAnimDur;
private final float mAlphaIn;
private final float mAlphaOut;
private Keyboard mAbcKeyboard;
private Keyboard mSymKeyboard;
private Keyboard mNumKeyboard;
// if we should capitalize the first letter in each sentence
private boolean mCapSentences;
// if we should capitalize the first letter in each word
private boolean mCapWords;
// if we should capitalize every character
private boolean mCapCharacters;
// if voice is on
private boolean mVoiceOn;
// Whether to allow escaping north or not
private boolean mEscapeNorthEnabled;
// Whether to dismiss when voice button is pressed
private boolean mVoiceKeyDismissesEnabled;
/**
* Voice
*/
private Intent mRecognizerIntent;
private SpeechRecognizer mSpeechRecognizer;
private SpeechLevelSource mSpeechLevelSource;
private RecognizerView mVoiceButtonView;
private class ScaleAnimation extends Animation {
private final ViewGroup.LayoutParams mParams;
private final View mView;
private float mStartX;
private float mStartY;
private float mStartWidth;
private float mStartHeight;
private float mEndX;
private float mEndY;
private float mEndWidth;
private float mEndHeight;
public ScaleAnimation(FrameLayout view) {
mView = view;
mParams = view.getLayoutParams();
setDuration(MOVEMENT_ANIMATION_DURATION);
setInterpolator(sMovementInterpolator);
}
public void setAnimationBounds(float x, float y, float width, float height) {
mEndX = x;
mEndY = y;
mEndWidth = width;
mEndHeight = height;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 0) {
mStartX = mView.getX();
mStartY = mView.getY();
mStartWidth = mParams.width;
mStartHeight = mParams.height;
} else {
setValues(((mEndX - mStartX) * interpolatedTime + mStartX),
((mEndY - mStartY) * interpolatedTime + mStartY),
((int)((mEndWidth - mStartWidth) * interpolatedTime + mStartWidth)),
((int)((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight)));
}
}
public void setValues(float x, float y, float width, float height) {
mView.setX(x);
mView.setY(y);
mParams.width = (int)(width);
mParams.height = (int)(height);
mView.setLayoutParams(mParams);
mView.requestLayout();
}
};
private AnimatorListener mVoiceEnterListener = new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mSelector.setVisibility(View.INVISIBLE);
startRecognition(mContext);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
};
private AnimatorListener mVoiceExitListener = new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mVoiceButtonView.showNotListening();
mSpeechRecognizer.cancel();
mSpeechRecognizer.setRecognitionListener(null);
mVoiceOn = false;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mSelector.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationCancel(Animator animation) {
}
};
private final VoiceIntroAnimator mVoiceAnimator;
// Tracks whether or not a touch event is in progress. This is true while
// a finger is down on the pad.
private boolean mTouchDown = false;
public LeanbackKeyboardContainer(Context context) {
mContext = (LeanbackImeService) context;
final Resources res = mContext.getResources();
mVoiceAnimDur = res.getInteger(R.integer.voice_anim_duration);
mAlphaIn = res.getFraction(R.fraction.alpha_in, 1, 1);
mAlphaOut = res.getFraction(R.fraction.alpha_out, 1, 1);
mVoiceAnimator = new VoiceIntroAnimator(mVoiceEnterListener, mVoiceExitListener);
initKeyboards();
mRootView = (RelativeLayout) mContext.getLayoutInflater()
.inflate(R.layout.root_leanback, null);
mKeyboardsContainer = mRootView.findViewById(R.id.keyboard);
mSuggestionsBg = mRootView.findViewById(R.id.candidate_background);
mSuggestionsContainer =
(HorizontalScrollView) mRootView.findViewById(R.id.suggestions_container);
mSuggestions = (LinearLayout) mSuggestionsContainer.findViewById(R.id.suggestions);
mMainKeyboardView = (LeanbackKeyboardView) mRootView.findViewById(R.id.main_keyboard);
mVoiceButtonView = (RecognizerView) mRootView.findViewById(R.id.voice);
mActionButtonView = (Button) mRootView.findViewById(R.id.enter);
mSelector = mRootView.findViewById(R.id.selector);
mSelectorAnimation = new ScaleAnimation((FrameLayout) mSelector);
mOverestimate = mContext.getResources().getFraction(R.fraction.focused_scale, 1, 1);
float scale = context.getResources().getFraction(R.fraction.clicked_scale, 1, 1);
mClickAnimDur = context.getResources().getInteger(R.integer.clicked_anim_duration);
mSelectorAnimator = ValueAnimator.ofFloat(1.0f, scale);
mSelectorAnimator.setDuration(mClickAnimDur);
mSelectorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float scale = (Float) animation.getAnimatedValue();
mSelector.setScaleX(scale);
mSelector.setScaleY(scale);
}
});
mSpeechLevelSource = new SpeechLevelSource();
mVoiceButtonView.setSpeechLevelSource(mSpeechLevelSource);
mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(mContext);
mVoiceButtonView.setCallback(new RecognizerView.Callback() {
@Override
public void onStartRecordingClicked() {
startVoiceRecording();
}
@Override
public void onStopRecordingClicked() {
cancelVoiceRecording();
}
@Override
public void onCancelRecordingClicked() {
cancelVoiceRecording();
}
});
}
public void startVoiceRecording() {
if (mVoiceEnabled) {
if (mVoiceKeyDismissesEnabled) {
if (DEBUG) Log.v(TAG, "Voice Dismiss");
mDismissListener.onDismiss(true);
} else {
mVoiceAnimator.startEnterAnimation();
}
}
}
public void cancelVoiceRecording() {
mVoiceAnimator.startExitAnimation();
}
public void resetVoice() {
mMainKeyboardView.setAlpha(mAlphaIn);
mActionButtonView.setAlpha(mAlphaIn);
mVoiceButtonView.setAlpha(mAlphaOut);
mMainKeyboardView.setVisibility(View.VISIBLE);
mActionButtonView.setVisibility(View.VISIBLE);
mVoiceButtonView.setVisibility(View.INVISIBLE);
}
public boolean isVoiceVisible() {
return mVoiceButtonView.getVisibility() == View.VISIBLE;
}
private void initKeyboards() {
Locale locale = Locale.getDefault();
if (isMatch(locale, LeanbackLocales.QWERTY_GB)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_en_gb);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_en_gb);
} else if (isMatch(locale, LeanbackLocales.QWERTY_IN)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_en_in);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_en_in);
} else if (isMatch(locale, LeanbackLocales.QWERTY_ES_EU)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_es_eu);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_ES_US)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_es_us);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_us);
} else if (isMatch(locale, LeanbackLocales.QWERTY_AZ)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_az);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_CA)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_ca);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_DA)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_da);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_ET)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_et);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_FI)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_fi);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_NB)) {
// in the LatinIME nb uses the US symbols (usd instead of euro)
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_nb);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_us);
} else if (isMatch(locale, LeanbackLocales.QWERTY_SV)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_sv);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTY_US)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_us);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_us);
} else if (isMatch(locale, LeanbackLocales.QWERTZ_CH)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwertz_ch);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.QWERTZ)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwertz);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
} else if (isMatch(locale, LeanbackLocales.AZERTY)) {
mAbcKeyboard = new Keyboard(mContext, R.xml.azerty);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_azerty);
} else {
mAbcKeyboard = new Keyboard(mContext, R.xml.qwerty_eu);
mSymKeyboard = new Keyboard(mContext, R.xml.sym_eu);
}
mNumKeyboard = new Keyboard(mContext, R.xml.number);
}
private boolean isMatch(Locale locale, Locale[] list) {
for (Locale compare : list) {
// comparison language is either blank or they match
if (TextUtils.isEmpty(compare.getLanguage()) ||
TextUtils.equals(locale.getLanguage(), compare.getLanguage())) {
// comparison country is either blank or they match
if (TextUtils.isEmpty(compare.getCountry()) ||
TextUtils.equals(locale.getCountry(), compare.getCountry())) {
return true;
}
}
}
return false;
}
/**
* This method is called when we start the input at a NEW input field to set up the IME options,
* such as suggestions, voice, and action
*/
public void onStartInput(EditorInfo attribute) {
setImeOptions(mContext.getResources(), attribute);
mVoiceOn = false;
}
/**
* This method is called whenever we bring up the IME at an input field.
*/
public void onStartInputView() {
// This must be done here because modifying the views before it is
// shown can cause selection handles to be shown if using a USB
// keyboard in a WebView.
clearSuggestions();
RelativeLayout.LayoutParams lp =
(RelativeLayout.LayoutParams) mKeyboardsContainer.getLayoutParams();
if (mSuggestionsEnabled) {
lp.removeRule(RelativeLayout.ALIGN_PARENT_TOP);
mSuggestionsContainer.setVisibility(View.VISIBLE);
mSuggestionsBg.setVisibility(View.VISIBLE);
} else {
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
mSuggestionsContainer.setVisibility(View.GONE);
mSuggestionsBg.setVisibility(View.GONE);
}
mKeyboardsContainer.setLayoutParams(lp);
mMainKeyboardView.setKeyboard(mInitialMainKeyboard);
// TODO fix this for number keyboard
mVoiceButtonView.setMicEnabled(mVoiceEnabled);
resetVoice();
dismissMiniKeyboard();
// setImeOptions will be called before this, setting the text resource value
if (!TextUtils.isEmpty(mEnterKeyText)) {
mActionButtonView.setText(mEnterKeyText);
mActionButtonView.setContentDescription(mEnterKeyText);
} else {
mActionButtonView.setText(mEnterKeyTextResId);
mActionButtonView.setContentDescription(mContext.getString(mEnterKeyTextResId));
}
if (mCapCharacters) {
setShiftState(LeanbackKeyboardView.SHIFT_LOCKED);
} else if (mCapSentences || mCapWords) {
setShiftState(LeanbackKeyboardView.SHIFT_ON);
} else {
setShiftState(LeanbackKeyboardView.SHIFT_OFF);
}
}
/**
* This method is called when the keyboard layout is complete, to set up the initial focus and
* visibility. This method gets called later than {@link onStartInput} and
* {@link onStartInputView}.
*/
public void onInitInputView() {
resetFocusCursor();
mSelector.setVisibility(View.VISIBLE);
}
public RelativeLayout getView() {
return mRootView;
}
public void setVoiceListener(VoiceListener listener) {
mVoiceListener = listener;
}
public void setDismissListener(DismissListener listener) {
mDismissListener = listener;
}
private void setImeOptions(Resources resources, EditorInfo attribute) {
mSuggestionsEnabled = true;
mAutoEnterSpaceEnabled = true;
mVoiceEnabled = true;
mInitialMainKeyboard = mAbcKeyboard;
mEscapeNorthEnabled = false;
mVoiceKeyDismissesEnabled = false;
// set keyboard properties
switch (LeanbackUtils.getInputTypeClass(attribute)) {
case EditorInfo.TYPE_CLASS_NUMBER:
case EditorInfo.TYPE_CLASS_DATETIME:
case EditorInfo.TYPE_CLASS_PHONE:
mSuggestionsEnabled = false;
mVoiceEnabled = false;
// TODO use number keyboard for these input types
mInitialMainKeyboard = mAbcKeyboard;
break;
case EditorInfo.TYPE_CLASS_TEXT:
switch (LeanbackUtils.getInputTypeVariation(attribute)) {
case EditorInfo.TYPE_TEXT_VARIATION_PASSWORD:
case EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD:
case EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:
case EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME:
mSuggestionsEnabled = false;
mVoiceEnabled = false;
mInitialMainKeyboard = mAbcKeyboard;
break;
case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
case EditorInfo.TYPE_TEXT_VARIATION_URI:
case EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT:
case EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
mSuggestionsEnabled = true;
mAutoEnterSpaceEnabled = false;
mVoiceEnabled = false;
mInitialMainKeyboard = mAbcKeyboard;
break;
}
break;
}
if (mSuggestionsEnabled) {
mSuggestionsEnabled = (attribute.inputType
& EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) == 0;
}
if (mAutoEnterSpaceEnabled) {
mAutoEnterSpaceEnabled = mSuggestionsEnabled && mAutoEnterSpaceEnabled;
}
mCapSentences = (attribute.inputType
& EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0;
mCapWords = ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) ||
(LeanbackUtils.getInputTypeVariation(attribute)
== EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME);
mCapCharacters = (attribute.inputType
& EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0;
if (attribute.privateImeOptions != null) {
if (attribute.privateImeOptions.contains(IME_PRIVATE_OPTIONS_ESCAPE_NORTH) ||
attribute.privateImeOptions.contains(
IME_PRIVATE_OPTIONS_ESCAPE_NORTH_LEGACY)) {
mEscapeNorthEnabled = true;
}
if (attribute.privateImeOptions.contains(IME_PRIVATE_OPTIONS_VOICE_DISMISS) ||
attribute.privateImeOptions.contains(
IME_PRIVATE_OPTIONS_VOICE_DISMISS_LEGACY)) {
mVoiceKeyDismissesEnabled = true;
}
}
if (DEBUG) {
Log.d(TAG, "sugg: " + mSuggestionsEnabled + " | capSentences: " + mCapSentences
+ " | capWords: " + mCapWords + " | capChar: " + mCapCharacters
+ " | escapeNorth: " + mEscapeNorthEnabled
+ " | voiceDismiss : " + mVoiceKeyDismissesEnabled
);
}
// set enter key
mEnterKeyText = attribute.actionLabel;
if (TextUtils.isEmpty(mEnterKeyText)) {
switch (LeanbackUtils.getImeAction(attribute)) {
case EditorInfo.IME_ACTION_GO:
mEnterKeyTextResId = R.string.label_go_key;
break;
case EditorInfo.IME_ACTION_NEXT:
mEnterKeyTextResId = R.string.label_next_key;
break;
case EditorInfo.IME_ACTION_SEARCH:
mEnterKeyTextResId = R.string.label_search_key;
break;
case EditorInfo.IME_ACTION_SEND:
mEnterKeyTextResId = R.string.label_send_key;
break;
default:
mEnterKeyTextResId = R.string.label_done_key;
break;
}
}
if (!VOICE_SUPPORTED) {
mVoiceEnabled = false;
}
}
public boolean isVoiceEnabled() {
return mVoiceEnabled;
}
public boolean areSuggestionsEnabled() {
return mSuggestionsEnabled;
}
public boolean enableAutoEnterSpace() {
return mAutoEnterSpaceEnabled;
}
private PointF getAlignmentPosition(float posXCm, float posYCm, PointF result) {
float width = mRootView.getWidth() - mRootView.getPaddingRight()
- mRootView.getPaddingLeft()
- mContext.getResources().getDimension(R.dimen.selector_size);
float height = mRootView.getHeight() - mRootView.getPaddingTop()
- mRootView.getPaddingBottom()
- mContext.getResources().getDimension(R.dimen.selector_size);
result.x = posXCm / PHYSICAL_WIDTH_CM * width + mRootView.getPaddingLeft();
result.y = posYCm / PHYSICAL_HEIGHT_CM * height + mRootView.getPaddingTop();
return result;
}
private void getPhysicalPosition(float x, float y, PointF result) {
x -= mSelector.getWidth() / 2;
y -= mSelector.getHeight() / 2;
float width = mRootView.getWidth() - mRootView.getPaddingRight()
- mRootView.getPaddingLeft()
- mContext.getResources().getDimension(R.dimen.selector_size);
float height = mRootView.getHeight() - mRootView.getPaddingTop()
- mRootView.getPaddingBottom()
- mContext.getResources().getDimension(R.dimen.selector_size);
float posXCm = (x - mRootView.getPaddingLeft()) * PHYSICAL_WIDTH_CM / width;
float posYCm = (y - mRootView.getPaddingTop()) * PHYSICAL_HEIGHT_CM / height;
result.x = posXCm;
result.y = posYCm;
}
private void offsetRect(Rect rect, View view) {
rect.left = 0;
rect.top = 0;
rect.right = view.getWidth();
rect.bottom = view.getHeight();
((ViewGroup) mRootView).offsetDescendantRectToMyCoords(view, rect);
}
/**
* Finds the {@link KeyFocus} on screen that best matches the given pixel positions
*
* @param x position in pixels, if null, use the last valid x value
* @param y position in pixels, if null, use the last valid y value
* @param focus the focus object to update with the result
* @return true if focus was successfully found, false otherwise.
*/
public boolean getBestFocus(Float x, Float y, KeyFocus focus) {
boolean validFocus = true;
offsetRect(mRect, mActionButtonView);
int actionLeft = mRect.left;
offsetRect(mRect, mMainKeyboardView);
int keyboardTop = mRect.top;
// use last if invalid
x = (x == null) ? mX : x;
y = (y == null) ? mY : y;
final int count = mSuggestions.getChildCount();
if (y < keyboardTop && count > 0 && mSuggestionsEnabled) {
for (int i = 0; i < count; i++) {
View suggestView = mSuggestions.getChildAt(i);
offsetRect(mRect, suggestView);
if (x < mRect.right || i+1 == count) {
suggestView.requestFocus();
LeanbackUtils.sendAccessibilityEvent(suggestView.findViewById(R.id.text), true);
configureFocus(focus, mRect, i, KeyFocus.TYPE_SUGGESTION);
break;
}
}
} else if (y < keyboardTop && mEscapeNorthEnabled) {
validFocus = false;
escapeNorth();
} else if (x > actionLeft) {
// closest is the action button
offsetRect(mRect, mActionButtonView);
configureFocus(focus, mRect, 0, KeyFocus.TYPE_ACTION);
} else {
mX = x;
mY = y;
// In the main view
offsetRect(mRect, mMainKeyboardView);
x = (x - mRect.left);
y = (y - mRect.top);
int index = mMainKeyboardView.getNearestIndex(x, y);
Key key = mMainKeyboardView.getKey(index);
configureFocus(focus, mRect, index, key, KeyFocus.TYPE_MAIN);
}
return validFocus;
}
private void escapeNorth() {
if (DEBUG) Log.v(TAG, "Escaping north");
mDismissListener.onDismiss(false);
}
private void configureFocus(KeyFocus focus, Rect rect, int index, int type) {
focus.type = type;
focus.index = index;
focus.rect.set(rect);
}
private void configureFocus(KeyFocus focus, Rect rect, int index, Key key, int type) {
focus.type = type;
if (key == null) {
return;
}
if (key.codes != null) {
focus.code = key.codes[0];
} else {
focus.code = KeyEvent.KEYCODE_UNKNOWN;
}
focus.index = index;
focus.label = key.label;
focus.rect.left = key.x + rect.left;
focus.rect.top = key.y + rect.top;
focus.rect.right = focus.rect.left + key.width;
focus.rect.bottom = focus.rect.top + key.height;
}
private void setKbFocus(KeyFocus focus, boolean forceFocusChange, boolean animate) {
if (focus.equals(mCurrKeyInfo) && !forceFocusChange) {
// Nothing changed
return;
}
LeanbackKeyboardView prevView = mPrevView;
mPrevView = null;
boolean overestimateWidth = false;
boolean overestimateHeight = false;
switch (focus.type) {
case KeyFocus.TYPE_VOICE:
mVoiceButtonView.setMicFocused(true);
dismissMiniKeyboard();
break;
case KeyFocus.TYPE_ACTION:
LeanbackUtils.sendAccessibilityEvent(mActionButtonView, true);
dismissMiniKeyboard();
break;
case KeyFocus.TYPE_SUGGESTION:
dismissMiniKeyboard();
break;
case KeyFocus.TYPE_MAIN:
overestimateHeight = true;
overestimateWidth = (focus.code != LeanbackKeyboardView.ASCII_SPACE);
mMainKeyboardView.setFocus(focus.index, mTouchState == TOUCH_STATE_CLICK, overestimateWidth);
mPrevView = mMainKeyboardView;
break;
}
if (prevView != null && prevView != mPrevView) {
prevView.setFocus(-1, mTouchState == TOUCH_STATE_CLICK);
}
setSelectorToFocus(focus.rect, overestimateWidth, overestimateHeight, animate);
mCurrKeyInfo.set(focus);
}
public void setSelectorToFocus(Rect rect, boolean overestimateWidth, boolean overestimateHeight,
boolean animate) {
if (mSelector.getWidth() == 0 || mSelector.getHeight() == 0
|| rect.width() == 0 || rect.height() == 0) {
return;
}
float width = rect.width();
float height = rect.height();
if (overestimateHeight) {
height *= mOverestimate;
}
if (overestimateWidth) {
width *= mOverestimate;
}
float major = Math.max(width, height);
float minor = Math.min(width, height);
// if the difference between the width and height is less than 10%,
// keep the width and height the same.
if (major / minor < 1.1) {
width = height = Math.max(width, height);
}
float x = rect.exactCenterX() - width/2;
float y = rect.exactCenterY() - height/2;
mSelectorAnimation.cancel();
if (animate) {
mSelectorAnimation.reset();
mSelectorAnimation.setAnimationBounds(x, y, width, height);
mSelector.startAnimation(mSelectorAnimation);
} else {
mSelectorAnimation.setValues(x, y, width, height);
}
}
public Keyboard.Key getKey(int type, int index) {
return (type == KeyFocus.TYPE_MAIN) ? mMainKeyboardView.getKey(index) : null;
}
public int getCurrKeyCode() {
Key key = getKey(mCurrKeyInfo.type, mCurrKeyInfo.index);
if (key != null) {
return key.codes[0];
}
return 0;
}
public int getTouchState() {
return mTouchState;
}
/**
* Set the view state which affects how the touch indicator is drawn. This code currently
* assumes the state changes below for simplicity. If the state machine is updated this code
* should probably be checked to ensure it still works. NO_TOUCH -> on touch start -> SNAP SNAP
* -> on enough movement -> MOVE MOVE -> on hover long enough -> SNAP SNAP -> on a click down ->
* CLICK CLICK -> on click released -> SNAP ANY STATE -> on touch end -> NO_TOUCH
*
* @param state The new state to transition to
*/
public void setTouchState(int state) {
switch (state) {
case TOUCH_STATE_NO_TOUCH:
if (mTouchState == TOUCH_STATE_TOUCH_MOVE || mTouchState == TOUCH_STATE_CLICK) {
// If the touch indicator was small make it big again
mSelectorAnimator.reverse();
}
break;
case TOUCH_STATE_TOUCH_SNAP:
if (mTouchState == TOUCH_STATE_CLICK) {
// And make the touch indicator big again
mSelectorAnimator.reverse();
} else if (mTouchState == TOUCH_STATE_TOUCH_MOVE) {
// Just make the touch indicator big
mSelectorAnimator.reverse();
}
break;
case TOUCH_STATE_TOUCH_MOVE:
if (mTouchState == TOUCH_STATE_NO_TOUCH || mTouchState == TOUCH_STATE_TOUCH_SNAP) {
// Shrink the touch indicator
mSelectorAnimator.start();
}
break;
case TOUCH_STATE_CLICK:
if (mTouchState == TOUCH_STATE_NO_TOUCH || mTouchState == TOUCH_STATE_TOUCH_SNAP) {
// Shrink the touch indicator
mSelectorAnimator.start();
}
break;
}
setTouchStateInternal(state);
setKbFocus(mCurrKeyInfo, true, true);
}
public KeyFocus getCurrFocus() {
return mCurrKeyInfo;
}
public void onVoiceClick() {
if (mVoiceButtonView != null) {
mVoiceButtonView.onClick();
}
}
public void onModeChangeClick() {
dismissMiniKeyboard();
if (mMainKeyboardView.getKeyboard().equals(mSymKeyboard)) {
mMainKeyboardView.setKeyboard(mAbcKeyboard);
} else {
mMainKeyboardView.setKeyboard(mSymKeyboard);
}
}
public void onShiftClick() {
setShiftState(mMainKeyboardView.isShifted() ? LeanbackKeyboardView.SHIFT_OFF
: LeanbackKeyboardView.SHIFT_ON);
}
public void onTextEntry() {
// reset shift if caps is not on
if (mMainKeyboardView.isShifted()) {
if (!isCapsLockOn() && !mCapCharacters) {
setShiftState(LeanbackKeyboardView.SHIFT_OFF);
}
} else {
if (isCapsLockOn() || mCapCharacters) {
setShiftState(LeanbackKeyboardView.SHIFT_LOCKED);
}
}
if (dismissMiniKeyboard()) {
moveFocusToIndex(mMiniKbKeyIndex, KeyFocus.TYPE_MAIN);
}
}
public void onSpaceEntry() {
if (mMainKeyboardView.isShifted()) {
if (!isCapsLockOn() && !mCapCharacters && !mCapWords) {
setShiftState(LeanbackKeyboardView.SHIFT_OFF);
}
} else {
if (isCapsLockOn() || mCapCharacters || mCapWords) {
setShiftState(LeanbackKeyboardView.SHIFT_ON);
}
}
}
public void onPeriodEntry() {
if (mMainKeyboardView.isShifted()) {
if (!isCapsLockOn() && !mCapCharacters && !mCapWords && !mCapSentences) {
setShiftState(LeanbackKeyboardView.SHIFT_OFF);
}
} else {
if (isCapsLockOn() || mCapCharacters || mCapWords || mCapSentences) {
setShiftState(LeanbackKeyboardView.SHIFT_ON);
}
}
}
public boolean dismissMiniKeyboard() {
return mMainKeyboardView.dismissMiniKeyboard();
}
public boolean isCurrKeyShifted() {
return mMainKeyboardView.isShifted();
}
public CharSequence getSuggestionText(int index) {
CharSequence text = null;
if(index >= 0 && index < mSuggestions.getChildCount()){
Button suggestion =
(Button) mSuggestions.getChildAt(index).findViewById(R.id.text);
if (suggestion != null) {
text = suggestion.getText();
}
}
return text;
}
/**
* This method sets the keyboard focus and update the layout of the new focus
*
* @param focus the new focus of the keyboard
*/
public void setFocus(KeyFocus focus) {
setKbFocus(focus, false, true);
}
public boolean getNextFocusInDirection(int direction, KeyFocus startFocus, KeyFocus nextFocus) {
boolean validNextFocus = true;
switch (startFocus.type) {
case KeyFocus.TYPE_VOICE:
// TODO move between voice button and kb button
break;
case KeyFocus.TYPE_ACTION:
offsetRect(mRect, mMainKeyboardView);
if ((direction & DIRECTION_LEFT) != 0) {
// y is null, so we use the last y. This way a user can hold left and wrap
// around the keyboard while staying in the same row
validNextFocus = getBestFocus((float) mRect.right, null, nextFocus);
} else if ((direction & DIRECTION_UP) != 0) {
offsetRect(mRect, mSuggestions);
validNextFocus = getBestFocus(
(float) startFocus.rect.centerX(), (float) mRect.centerY(), nextFocus);
}
break;
case KeyFocus.TYPE_SUGGESTION:
if ((direction & DIRECTION_DOWN) != 0) {
offsetRect(mRect, mMainKeyboardView);
validNextFocus = getBestFocus(
(float) startFocus.rect.centerX(), (float) mRect.top, nextFocus);
} else if ((direction & DIRECTION_UP) != 0) {
if (mEscapeNorthEnabled) {
escapeNorth();
}
} else {
boolean left = (direction & DIRECTION_LEFT) != 0;
boolean right = (direction & DIRECTION_RIGHT) != 0;
if (left || right) {
// Cannot offset on the suggestion container because as it scrolls those
// values change
offsetRect(mRect, mRootView);
MarginLayoutParams lp =
(MarginLayoutParams) mSuggestionsContainer.getLayoutParams();
int leftSide = mRect.left + lp.leftMargin;
int rightSide = mRect.right - lp.rightMargin;
int index = startFocus.index + (left ? -1 : 1);
View suggestView = mSuggestions.getChildAt(index);
if (suggestView != null) {
offsetRect(mRect, suggestView);
if (mRect.left < leftSide && mRect.right > rightSide) {
mRect.left = leftSide;
mRect.right = rightSide;
} else if (mRect.left < leftSide) {
mRect.right = leftSide + mRect.width();
mRect.left = leftSide;
} else if (mRect.right > rightSide) {
mRect.left = rightSide - mRect.width();
mRect.right = rightSide;
}
suggestView.requestFocus();
LeanbackUtils.sendAccessibilityEvent(
suggestView.findViewById(R.id.text), true);
configureFocus(nextFocus, mRect, index, KeyFocus.TYPE_SUGGESTION);
}
}
}
break;
case KeyFocus.TYPE_MAIN:
Key key = getKey(startFocus.type, startFocus.index);
// Step within the view. Using height because all keys are the same height
// and widths vary. Half the height is to ensure the next key is reached
float extraSlide = startFocus.rect.height()/2.0f;
float x = startFocus.rect.centerX();
float y = startFocus.rect.centerY();
if (startFocus.code == LeanbackKeyboardView.ASCII_SPACE) {
// if we're moving off of space, use the old x position for memory
x = mX;
}
if ((direction & DIRECTION_LEFT) != 0) {
if ((key.edgeFlags & Keyboard.EDGE_LEFT) == 0) {
// not on the left edge of the kb
x = startFocus.rect.left - extraSlide;
}
} else if ((direction & DIRECTION_RIGHT) != 0) {
if ((key.edgeFlags & Keyboard.EDGE_RIGHT) != 0) {
// jump to the action button
offsetRect(mRect, mActionButtonView);
x = mRect.centerX();
} else {
x = startFocus.rect.right + extraSlide;
}
}
// Don't need any special handling for up/down due to
// layout positioning. If the layout changes this should be
// reconsidered.
if ((direction & DIRECTION_UP) != 0) {
y -= startFocus.rect.height() * DIRECTION_STEP_MULTIPLIER;
} else if ((direction & DIRECTION_DOWN) != 0) {
y += startFocus.rect.height() * DIRECTION_STEP_MULTIPLIER;
}
getPhysicalPosition(x, y, mTempPoint);
validNextFocus = getBestFocus(x, y, nextFocus);
break;
}
return validNextFocus;
}
private PointF getTouchSnapPosition() {
PointF snapPos = new PointF();
getPhysicalPosition(mCurrKeyInfo.rect.centerX(), mCurrKeyInfo.rect.centerY(), snapPos);
return snapPos;
}
public void clearSuggestions() {
mSuggestions.removeAllViews();
if (getCurrFocus().type == KeyFocus.TYPE_SUGGESTION) {
resetFocusCursor();
}
}
public void updateSuggestions(ArrayList<String> suggestions) {
final int oldCount = mSuggestions.getChildCount();
final int newCount = suggestions.size();
if (newCount < oldCount) {
// remove excess views
mSuggestions.removeViews(newCount, oldCount-newCount);
} else if (newCount > oldCount) {
// add more
for (int i = oldCount; i < newCount; i++) {
View suggestion = mContext.getLayoutInflater()
.inflate(R.layout.candidate, null);
mSuggestions.addView(suggestion);
}
}
for (int i = 0; i < newCount; i++) {
Button suggestion =
(Button) mSuggestions.getChildAt(i).findViewById(R.id.text);
suggestion.setText(suggestions.get(i));
suggestion.setContentDescription(suggestions.get(i));
}
if (getCurrFocus().type == KeyFocus.TYPE_SUGGESTION) {
resetFocusCursor();
}
}
/**
* Moves the selector back to the entry point key (T in general)
*/
public void resetFocusCursor() {
// T is the best starting letter, it's in the 5th column and 2nd row,
// this approximates that location
double x = 0.45;
double y = 0.375;
offsetRect(mRect, mMainKeyboardView);
mX = (float)(mRect.left + x*mRect.width());
mY = (float)(mRect.top + y*mRect.height());
getBestFocus(mX, mY, mTempKeyInfo);
setKbFocus(mTempKeyInfo, true, false);
setTouchStateInternal(TOUCH_STATE_NO_TOUCH);
mSelectorAnimator.reverse();
mSelectorAnimator.end();
}
private void setTouchStateInternal(int state) {
mTouchState = state;
}
private void setShiftState(int state) {
mMainKeyboardView.setShiftState(state);
}
private void startRecognition(Context context) {
mRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
mRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
mRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
float peakRmsLevel = 0;
int rmsCounter = 0;
@Override
public void onBeginningOfSpeech() {
mVoiceButtonView.showRecording();
}
@Override
public void onEndOfSpeech() {
mVoiceButtonView.showRecognizing();
mVoiceOn = false;
}
@Override
public void onError(int error) {
cancelVoiceRecording();
switch (error) {
case SpeechRecognizer.ERROR_NO_MATCH:
Log.d(TAG, "recognizer error no match");
break;
case SpeechRecognizer.ERROR_SERVER:
Log.d(TAG, "recognizer error server error");
break;
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
Log.d(TAG, "recognizer error speech timeout");
break;
case SpeechRecognizer.ERROR_CLIENT:
Log.d(TAG, "recognizer error client error");
break;
default:
Log.d(TAG, "recognizer other error " + error);
break;
}
}
@Override
public synchronized void onPartialResults(Bundle partialResults) {
}
@Override
public void onReadyForSpeech(Bundle params) {
mVoiceButtonView.showListening();
}
@Override
public void onEvent(int eventType, Bundle params) {
}
@Override
public void onBufferReceived(byte[] buffer) {
}
@Override
public synchronized void onRmsChanged(float rmsdB) {
mVoiceOn = true;
mSpeechLevelSource.setSpeechLevel((rmsdB < 0) ? 0 : (int) (10 * rmsdB));
peakRmsLevel = Math.max(rmsdB, peakRmsLevel);
rmsCounter++;
if (rmsCounter > 100 && peakRmsLevel == 0) {
mVoiceButtonView.showNotListening();
}
}
@Override
public void onResults(Bundle results) {
final ArrayList<String> matches =
results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
if (matches != null) {
if (mVoiceListener != null) {
mVoiceListener.onVoiceResult(matches.get(0));
}
}
cancelVoiceRecording();
}
});
mSpeechRecognizer.startListening(mRecognizerIntent);
}
public boolean isMiniKeyboardOnScreen() {
return mMainKeyboardView.isMiniKeyboardOnScreen();
}
public boolean onKeyLongPress() {
if (mCurrKeyInfo.code == Keyboard.KEYCODE_SHIFT) {
onToggleCapsLock();
setTouchState(TOUCH_STATE_NO_TOUCH);
return true;
} else if (mCurrKeyInfo.type == KeyFocus.TYPE_MAIN) {
mMainKeyboardView.onKeyLongPress();
if (mMainKeyboardView.isMiniKeyboardOnScreen()) {
mMiniKbKeyIndex = mCurrKeyInfo.index;
moveFocusToIndex(mMainKeyboardView.getBaseMiniKbIndex(), KeyFocus.TYPE_MAIN);
return true;
}
}
return false;
}
private void moveFocusToIndex(int index, int type) {
Key key = mMainKeyboardView.getKey(index);
configureFocus(mTempKeyInfo, mRect, index, key, type);
setTouchState(TOUCH_STATE_NO_TOUCH);
setKbFocus(mTempKeyInfo, true, true);
}
private void onToggleCapsLock() {
onShiftDoubleClick(isCapsLockOn());
}
public void onShiftDoubleClick(boolean wasCapsLockOn) {
setShiftState(
wasCapsLockOn ? LeanbackKeyboardView.SHIFT_OFF : LeanbackKeyboardView.SHIFT_LOCKED);
}
public boolean isCapsLockOn() {
return mMainKeyboardView.getShiftState() == LeanbackKeyboardView.SHIFT_LOCKED;
}
}