/* * 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); } }