1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.inputmethod.leanback.service; 18 19 import android.content.Intent; 20 import android.inputmethodservice.InputMethodService; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.view.InputDevice; 25 import android.view.KeyEvent; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.inputmethod.CompletionInfo; 29 import android.view.inputmethod.EditorInfo; 30 import android.view.inputmethod.InputConnection; 31 import android.util.Log; 32 33 import com.android.inputmethod.leanback.LeanbackKeyboardContainer; 34 import com.android.inputmethod.leanback.LeanbackKeyboardController; 35 import com.android.inputmethod.leanback.LeanbackKeyboardView; 36 import com.android.inputmethod.leanback.LeanbackLocales; 37 import com.android.inputmethod.leanback.LeanbackSuggestionsFactory; 38 import com.android.inputmethod.leanback.LeanbackUtils; 39 40 /** 41 * This is a simplified version of GridIme 42 */ 43 public class LeanbackImeService extends InputMethodService { 44 45 private static final String TAG = "LbImeService"; 46 private static final boolean DEBUG = false; 47 48 // use dpad events, with lock axis 49 static final int MODE_TRACKPAD_NAVIGATION = 0; 50 // track motion directly. 51 static final int MODE_FREE_MOVEMENT = 1; 52 53 public static final int MAX_SUGGESTIONS = 10; 54 55 private static final int MSG_SUGGESTIONS_CLEAR = 123; 56 private static final int SUGGESTIONS_CLEAR_DELAY = 1000; 57 58 public static final String IME_OPEN = "com.android.inputmethod.leanback.action.IME_OPEN"; 59 public static final String IME_CLOSE = "com.android.inputmethod.leanback.action.IME_CLOSE"; 60 61 private LeanbackKeyboardController.InputListener mInputListener 62 = new LeanbackKeyboardController.InputListener() { 63 @Override 64 public void onEntry(int type, int keyCode, CharSequence result) { 65 handleTextEntry(type, keyCode, result); 66 } 67 }; 68 69 private View mInputView; 70 private LeanbackKeyboardController mKeyboardController; 71 private LeanbackSuggestionsFactory mSuggestionsFactory; 72 73 // IME will auto insert space after clicking on the candidates if next 74 // character is alphabet 75 private boolean mEnterSpaceBeforeCommitting; 76 77 private boolean mShouldClearSuggestions = true; 78 private final Handler mHandler = new Handler() { 79 @Override 80 public void handleMessage(Message msg) { 81 if (msg.what == MSG_SUGGESTIONS_CLEAR) { 82 if (mShouldClearSuggestions) { 83 mSuggestionsFactory.clearSuggestions(); 84 mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); 85 mShouldClearSuggestions = false; 86 } 87 } 88 } 89 }; 90 LeanbackImeService()91 public LeanbackImeService() { 92 if (!enableHardwareAcceleration()) { 93 Log.w(TAG, "Could not enable hardware acceleration"); 94 } 95 } 96 clearSuggestionsDelayed()97 private void clearSuggestionsDelayed() { 98 // if suggestions amend, we should keep clearing them 99 if (!mSuggestionsFactory.shouldSuggestionsAmend()) { 100 mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR); 101 mShouldClearSuggestions = true; 102 mHandler.sendEmptyMessageDelayed(MSG_SUGGESTIONS_CLEAR, SUGGESTIONS_CLEAR_DELAY); 103 } 104 } 105 106 @Override onInitializeInterface()107 public void onInitializeInterface() { 108 mKeyboardController = new LeanbackKeyboardController(this, mInputListener); 109 mEnterSpaceBeforeCommitting = false; 110 mSuggestionsFactory = new LeanbackSuggestionsFactory(this, MAX_SUGGESTIONS); 111 } 112 113 @Override onCreateInputView()114 public View onCreateInputView() { 115 mInputView = mKeyboardController.getView(); 116 mInputView.requestFocus(); 117 return mInputView; 118 } 119 120 /** 121 * {@inheritDoc} This function gets called whenever we start the input 122 * window 123 */ 124 @Override onStartInputView(EditorInfo info, boolean restarting)125 public void onStartInputView(EditorInfo info, boolean restarting) { 126 super.onStartInputView(info, restarting); 127 mKeyboardController.onStartInputView(); 128 sendBroadcast(new Intent(IME_OPEN)); 129 130 if (mKeyboardController.areSuggestionsEnabled()) { 131 mSuggestionsFactory.createSuggestions(); 132 mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); 133 134 // repost text to get completions 135 InputConnection ic = getCurrentInputConnection(); 136 if (ic != null) { 137 String c = getEditorText(ic); 138 ic.deleteSurroundingText(getCharLengthBeforeCursor(ic), 139 getCharLengthAfterCursor(ic)); 140 ic.commitText(c, 1); 141 } 142 } 143 } 144 145 146 @Override onFinishInputView(boolean finishingInput)147 public void onFinishInputView(boolean finishingInput) { 148 super.onFinishInputView(finishingInput); 149 sendBroadcast(new Intent(IME_CLOSE)); 150 mSuggestionsFactory.clearSuggestions(); 151 } 152 153 /** 154 * {@inheritDoc} This function doesn't get called when we dismiss the 155 * keyboard, and reopen it on the same input field 156 */ 157 @Override onStartInput(EditorInfo attribute, boolean restarting)158 public void onStartInput(EditorInfo attribute, boolean restarting) { 159 super.onStartInput(attribute, restarting); 160 mEnterSpaceBeforeCommitting = false; 161 mSuggestionsFactory.onStartInput(attribute); 162 mKeyboardController.onStartInput(attribute); 163 } 164 165 /** 166 * {@inheritDoc} Always return true to show GridIme when editText calls 167 * requestFocus 168 */ 169 @Override onShowInputRequested(int flags, boolean configChange)170 public boolean onShowInputRequested(int flags, boolean configChange) { 171 return true; 172 } 173 174 /** 175 * {@inheritDoc} Always enable soft keyboard. If we return the super method, 176 * the IME will not be shown if there is a hardware keyboard connected 177 */ 178 @Override onEvaluateInputViewShown()179 public boolean onEvaluateInputViewShown() { 180 return true; 181 } 182 183 @Override onEvaluateFullscreenMode()184 public boolean onEvaluateFullscreenMode() { 185 // Superclass always returns true in landscape mode. 186 // Assume we're on TV with lots of display area. 187 return false; 188 } 189 190 @Override onKeyUp(int keyCode, KeyEvent event)191 public boolean onKeyUp(int keyCode, KeyEvent event) { 192 if (isInputViewShown() 193 && mKeyboardController.onKeyUp(keyCode, event)) { 194 return true; 195 } 196 return super.onKeyUp(keyCode, event); 197 } 198 199 @Override onKeyDown(int keyCode, KeyEvent event)200 public boolean onKeyDown(int keyCode, KeyEvent event) { 201 if (isInputViewShown() 202 && mKeyboardController.onKeyDown(keyCode, event)) { 203 return true; 204 } 205 return super.onKeyDown(keyCode, event); 206 } 207 208 @Override onGenericMotionEvent(MotionEvent event)209 public boolean onGenericMotionEvent(MotionEvent event) { 210 if (isInputViewShown() && (event.getSource() & InputDevice.SOURCE_TOUCH_NAVIGATION) 211 == InputDevice.SOURCE_TOUCH_NAVIGATION) { 212 if (mKeyboardController.onGenericMotionEvent(event)) { 213 return true; 214 } 215 } 216 return super.onGenericMotionEvent(event); 217 } 218 219 @Override onDisplayCompletions(CompletionInfo[] completions)220 public void onDisplayCompletions(CompletionInfo[] completions) { 221 if (mKeyboardController.areSuggestionsEnabled()) { 222 mShouldClearSuggestions = false; 223 mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR); 224 mSuggestionsFactory.onDisplayCompletions(completions); 225 mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); 226 } 227 } 228 getEditorText(InputConnection ic)229 private String getEditorText(InputConnection ic) { 230 StringBuilder editorText = new StringBuilder(); 231 CharSequence textBeforeCursor = ic.getTextBeforeCursor(1000, 0); 232 CharSequence textAfterCursor = ic.getTextAfterCursor(1000, 0); 233 if (textBeforeCursor != null) { 234 editorText.append(textBeforeCursor); 235 } 236 if (textAfterCursor != null) { 237 editorText.append(textAfterCursor); 238 } 239 return editorText.toString(); 240 } 241 getAmpersandLocation(InputConnection ic)242 private int getAmpersandLocation(InputConnection ic) { 243 String editorText = getEditorText(ic); 244 int indexOf = editorText.indexOf('@'); 245 if (indexOf < 0) { 246 indexOf = editorText.length(); 247 } 248 249 return indexOf; 250 } 251 getCharLengthBeforeCursor(InputConnection ic)252 private int getCharLengthBeforeCursor(InputConnection ic) { 253 final CharSequence textLeft = ic.getTextBeforeCursor(1000, 0); 254 return textLeft != null ? textLeft.length() : 0; 255 } 256 getCharLengthAfterCursor(InputConnection ic )257 private int getCharLengthAfterCursor(InputConnection ic ) { 258 final CharSequence textRight = ic.getTextAfterCursor(1000, 0); 259 return textRight != null ? textRight.length() : 0; 260 } 261 handleTextEntry(int type, int keyCode, CharSequence c)262 private void handleTextEntry(int type, int keyCode, CharSequence c) { 263 InputConnection ic = getCurrentInputConnection(); 264 boolean updateSuggestions = true; 265 266 if (ic == null) { 267 return; 268 } 269 270 switch (type) { 271 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_BACKSPACE: 272 clearSuggestionsDelayed(); 273 ic.deleteSurroundingText(1, 0); 274 mEnterSpaceBeforeCommitting = false; 275 break; 276 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_LEFT: 277 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_RIGHT: 278 CharSequence textBeforeCursor = ic.getTextBeforeCursor(1000, 0); 279 int newCursorPosition = textBeforeCursor == null ? 0 : textBeforeCursor.length(); 280 281 if (type == LeanbackKeyboardController.InputListener.ENTRY_TYPE_LEFT) { 282 if (newCursorPosition > 0) { 283 newCursorPosition--; 284 } 285 } else { 286 CharSequence textAfterCursor = ic.getTextAfterCursor(1000, 0); 287 if (textAfterCursor != null && textAfterCursor.length() > 0) { 288 newCursorPosition++; 289 } 290 } 291 292 ic.setSelection(newCursorPosition, newCursorPosition); 293 break; 294 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_STRING: 295 clearSuggestionsDelayed(); 296 if (mEnterSpaceBeforeCommitting 297 && mKeyboardController.enableAutoEnterSpace()) { 298 if (LeanbackUtils.isAlphabet(keyCode)) { 299 ic.commitText(" ", 1); 300 } 301 mEnterSpaceBeforeCommitting = false; 302 } 303 ic.commitText(c, 1); 304 if (keyCode == LeanbackKeyboardView.ASCII_PERIOD) { 305 mEnterSpaceBeforeCommitting = true; 306 } 307 break; 308 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_SUGGESTION: 309 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_VOICE: 310 clearSuggestionsDelayed(); 311 if (!mSuggestionsFactory.shouldSuggestionsAmend()) { 312 ic.deleteSurroundingText(getCharLengthBeforeCursor(ic), 313 getCharLengthAfterCursor(ic)); 314 } else { 315 int location = getAmpersandLocation(ic); 316 ic.setSelection(location, location); 317 ic.deleteSurroundingText(0, getCharLengthAfterCursor(ic)); 318 } 319 ic.commitText(c, 1); 320 mEnterSpaceBeforeCommitting = true; 321 // go straight into action (skip updating suggestions) 322 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_ACTION: 323 sendDefaultEditorAction(false); 324 updateSuggestions = false; 325 break; 326 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_DISMISS: 327 ic.performEditorAction(EditorInfo.IME_ACTION_NONE); 328 updateSuggestions = false; 329 break; 330 case LeanbackKeyboardController.InputListener.ENTRY_TYPE_VOICE_DISMISS: 331 ic.performEditorAction(EditorInfo.IME_ACTION_GO); 332 updateSuggestions = false; 333 break; 334 } 335 336 if (mKeyboardController.areSuggestionsEnabled() && updateSuggestions) { 337 mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); 338 } 339 } 340 onHideIme()341 public void onHideIme() { 342 requestHideSelf(0); 343 } 344 } 345