| /* |
| * 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.service; |
| |
| import android.content.Intent; |
| import android.inputmethodservice.InputMethodService; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.view.InputDevice; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.inputmethod.CompletionInfo; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputConnection; |
| import android.util.Log; |
| |
| import com.android.inputmethod.leanback.LeanbackKeyboardContainer; |
| import com.android.inputmethod.leanback.LeanbackKeyboardController; |
| import com.android.inputmethod.leanback.LeanbackKeyboardView; |
| import com.android.inputmethod.leanback.LeanbackLocales; |
| import com.android.inputmethod.leanback.LeanbackSuggestionsFactory; |
| import com.android.inputmethod.leanback.LeanbackUtils; |
| |
| /** |
| * This is a simplified version of GridIme |
| */ |
| public class LeanbackImeService extends InputMethodService { |
| |
| private static final String TAG = "LbImeService"; |
| private static final boolean DEBUG = false; |
| |
| // use dpad events, with lock axis |
| static final int MODE_TRACKPAD_NAVIGATION = 0; |
| // track motion directly. |
| static final int MODE_FREE_MOVEMENT = 1; |
| |
| public static final int MAX_SUGGESTIONS = 10; |
| |
| private static final int MSG_SUGGESTIONS_CLEAR = 123; |
| private static final int SUGGESTIONS_CLEAR_DELAY = 1000; |
| |
| public static final String IME_OPEN = "com.android.inputmethod.leanback.action.IME_OPEN"; |
| public static final String IME_CLOSE = "com.android.inputmethod.leanback.action.IME_CLOSE"; |
| |
| private LeanbackKeyboardController.InputListener mInputListener |
| = new LeanbackKeyboardController.InputListener() { |
| @Override |
| public void onEntry(int type, int keyCode, CharSequence result) { |
| handleTextEntry(type, keyCode, result); |
| } |
| }; |
| |
| private View mInputView; |
| private LeanbackKeyboardController mKeyboardController; |
| private LeanbackSuggestionsFactory mSuggestionsFactory; |
| |
| // IME will auto insert space after clicking on the candidates if next |
| // character is alphabet |
| private boolean mEnterSpaceBeforeCommitting; |
| |
| private boolean mShouldClearSuggestions = true; |
| private final Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| if (msg.what == MSG_SUGGESTIONS_CLEAR) { |
| if (mShouldClearSuggestions) { |
| mSuggestionsFactory.clearSuggestions(); |
| mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); |
| mShouldClearSuggestions = false; |
| } |
| } |
| } |
| }; |
| |
| public LeanbackImeService() { |
| if (!enableHardwareAcceleration()) { |
| Log.w(TAG, "Could not enable hardware acceleration"); |
| } |
| } |
| |
| private void clearSuggestionsDelayed() { |
| // if suggestions amend, we should keep clearing them |
| if (!mSuggestionsFactory.shouldSuggestionsAmend()) { |
| mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR); |
| mShouldClearSuggestions = true; |
| mHandler.sendEmptyMessageDelayed(MSG_SUGGESTIONS_CLEAR, SUGGESTIONS_CLEAR_DELAY); |
| } |
| } |
| |
| @Override |
| public void onInitializeInterface() { |
| mKeyboardController = new LeanbackKeyboardController(this, mInputListener); |
| mEnterSpaceBeforeCommitting = false; |
| mSuggestionsFactory = new LeanbackSuggestionsFactory(this, MAX_SUGGESTIONS); |
| } |
| |
| @Override |
| public View onCreateInputView() { |
| mInputView = mKeyboardController.getView(); |
| mInputView.requestFocus(); |
| return mInputView; |
| } |
| |
| /** |
| * {@inheritDoc} This function gets called whenever we start the input |
| * window |
| */ |
| @Override |
| public void onStartInputView(EditorInfo info, boolean restarting) { |
| super.onStartInputView(info, restarting); |
| mKeyboardController.onStartInputView(); |
| sendBroadcast(new Intent(IME_OPEN)); |
| |
| if (mKeyboardController.areSuggestionsEnabled()) { |
| mSuggestionsFactory.createSuggestions(); |
| mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); |
| |
| // repost text to get completions |
| InputConnection ic = getCurrentInputConnection(); |
| if (ic != null) { |
| String c = getEditorText(ic); |
| ic.deleteSurroundingText(getCharLengthBeforeCursor(ic), |
| getCharLengthAfterCursor(ic)); |
| ic.commitText(c, 1); |
| } |
| } |
| } |
| |
| |
| @Override |
| public void onFinishInputView(boolean finishingInput) { |
| super.onFinishInputView(finishingInput); |
| sendBroadcast(new Intent(IME_CLOSE)); |
| mSuggestionsFactory.clearSuggestions(); |
| } |
| |
| /** |
| * {@inheritDoc} This function doesn't get called when we dismiss the |
| * keyboard, and reopen it on the same input field |
| */ |
| @Override |
| public void onStartInput(EditorInfo attribute, boolean restarting) { |
| super.onStartInput(attribute, restarting); |
| mEnterSpaceBeforeCommitting = false; |
| mSuggestionsFactory.onStartInput(attribute); |
| mKeyboardController.onStartInput(attribute); |
| } |
| |
| /** |
| * {@inheritDoc} Always return true to show GridIme when editText calls |
| * requestFocus |
| */ |
| @Override |
| public boolean onShowInputRequested(int flags, boolean configChange) { |
| return true; |
| } |
| |
| /** |
| * {@inheritDoc} Always enable soft keyboard. If we return the super method, |
| * the IME will not be shown if there is a hardware keyboard connected |
| */ |
| @Override |
| public boolean onEvaluateInputViewShown() { |
| return true; |
| } |
| |
| @Override |
| public boolean onEvaluateFullscreenMode() { |
| // Superclass always returns true in landscape mode. |
| // Assume we're on TV with lots of display area. |
| return false; |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| if (isInputViewShown() |
| && mKeyboardController.onKeyUp(keyCode, event)) { |
| return true; |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (isInputViewShown() |
| && mKeyboardController.onKeyDown(keyCode, event)) { |
| return true; |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onGenericMotionEvent(MotionEvent event) { |
| if (isInputViewShown() && (event.getSource() & InputDevice.SOURCE_TOUCH_NAVIGATION) |
| == InputDevice.SOURCE_TOUCH_NAVIGATION) { |
| if (mKeyboardController.onGenericMotionEvent(event)) { |
| return true; |
| } |
| } |
| return super.onGenericMotionEvent(event); |
| } |
| |
| @Override |
| public void onDisplayCompletions(CompletionInfo[] completions) { |
| if (mKeyboardController.areSuggestionsEnabled()) { |
| mShouldClearSuggestions = false; |
| mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR); |
| mSuggestionsFactory.onDisplayCompletions(completions); |
| mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); |
| } |
| } |
| |
| private String getEditorText(InputConnection ic) { |
| StringBuilder editorText = new StringBuilder(); |
| CharSequence textBeforeCursor = ic.getTextBeforeCursor(1000, 0); |
| CharSequence textAfterCursor = ic.getTextAfterCursor(1000, 0); |
| if (textBeforeCursor != null) { |
| editorText.append(textBeforeCursor); |
| } |
| if (textAfterCursor != null) { |
| editorText.append(textAfterCursor); |
| } |
| return editorText.toString(); |
| } |
| |
| private int getAmpersandLocation(InputConnection ic) { |
| String editorText = getEditorText(ic); |
| int indexOf = editorText.indexOf('@'); |
| if (indexOf < 0) { |
| indexOf = editorText.length(); |
| } |
| |
| return indexOf; |
| } |
| |
| private int getCharLengthBeforeCursor(InputConnection ic) { |
| final CharSequence textLeft = ic.getTextBeforeCursor(1000, 0); |
| return textLeft != null ? textLeft.length() : 0; |
| } |
| |
| private int getCharLengthAfterCursor(InputConnection ic ) { |
| final CharSequence textRight = ic.getTextAfterCursor(1000, 0); |
| return textRight != null ? textRight.length() : 0; |
| } |
| |
| private void handleTextEntry(int type, int keyCode, CharSequence c) { |
| InputConnection ic = getCurrentInputConnection(); |
| boolean updateSuggestions = true; |
| |
| if (ic == null) { |
| return; |
| } |
| |
| switch (type) { |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_BACKSPACE: |
| clearSuggestionsDelayed(); |
| ic.deleteSurroundingText(1, 0); |
| mEnterSpaceBeforeCommitting = false; |
| break; |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_LEFT: |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_RIGHT: |
| CharSequence textBeforeCursor = ic.getTextBeforeCursor(1000, 0); |
| int newCursorPosition = textBeforeCursor == null ? 0 : textBeforeCursor.length(); |
| |
| if (type == LeanbackKeyboardController.InputListener.ENTRY_TYPE_LEFT) { |
| if (newCursorPosition > 0) { |
| newCursorPosition--; |
| } |
| } else { |
| CharSequence textAfterCursor = ic.getTextAfterCursor(1000, 0); |
| if (textAfterCursor != null && textAfterCursor.length() > 0) { |
| newCursorPosition++; |
| } |
| } |
| |
| ic.setSelection(newCursorPosition, newCursorPosition); |
| break; |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_STRING: |
| clearSuggestionsDelayed(); |
| if (mEnterSpaceBeforeCommitting |
| && mKeyboardController.enableAutoEnterSpace()) { |
| if (LeanbackUtils.isAlphabet(keyCode)) { |
| ic.commitText(" ", 1); |
| } |
| mEnterSpaceBeforeCommitting = false; |
| } |
| ic.commitText(c, 1); |
| if (keyCode == LeanbackKeyboardView.ASCII_PERIOD) { |
| mEnterSpaceBeforeCommitting = true; |
| } |
| break; |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_SUGGESTION: |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_VOICE: |
| clearSuggestionsDelayed(); |
| if (!mSuggestionsFactory.shouldSuggestionsAmend()) { |
| ic.deleteSurroundingText(getCharLengthBeforeCursor(ic), |
| getCharLengthAfterCursor(ic)); |
| } else { |
| int location = getAmpersandLocation(ic); |
| ic.setSelection(location, location); |
| ic.deleteSurroundingText(0, getCharLengthAfterCursor(ic)); |
| } |
| ic.commitText(c, 1); |
| mEnterSpaceBeforeCommitting = true; |
| // go straight into action (skip updating suggestions) |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_ACTION: |
| sendDefaultEditorAction(false); |
| updateSuggestions = false; |
| break; |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_DISMISS: |
| ic.performEditorAction(EditorInfo.IME_ACTION_NONE); |
| updateSuggestions = false; |
| break; |
| case LeanbackKeyboardController.InputListener.ENTRY_TYPE_VOICE_DISMISS: |
| ic.performEditorAction(EditorInfo.IME_ACTION_GO); |
| updateSuggestions = false; |
| break; |
| } |
| |
| if (mKeyboardController.areSuggestionsEnabled() && updateSuggestions) { |
| mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); |
| } |
| } |
| |
| public void onHideIme() { |
| requestHideSelf(0); |
| } |
| } |