/* * 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: *
* | 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 | | **/ 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