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; 18 19 import android.graphics.PointF; 20 import android.inputmethodservice.InputMethodService; 21 import android.inputmethodservice.Keyboard; 22 import android.inputmethodservice.Keyboard.Key; 23 import android.util.Log; 24 import android.view.KeyEvent; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.inputmethod.EditorInfo; 28 29 import com.android.inputmethod.leanback.LeanbackKeyboardContainer.KeyFocus; 30 31 import java.util.ArrayList; 32 /** 33 * Holds logic for the keyboard views. This includes things like when to 34 * snap, when to switch keyboards, etc. It provides callbacks for when actions 35 * that need to be handled at the IME level occur (when text is entered, when 36 * the action should be performed). 37 */ 38 public class LeanbackKeyboardController implements LeanbackKeyboardContainer.VoiceListener, 39 LeanbackKeyboardContainer.DismissListener { 40 private static final String TAG = "LbKbController"; 41 private static final boolean DEBUG = false; 42 43 /** 44 * The amount of time to block movement after a button down was detected. 45 */ 46 public static final int CLICK_MOVEMENT_BLOCK_DURATION_MS = 500; 47 48 /** 49 * The minimum distance in pixels before the view will transition to the 50 * move state. 51 */ 52 public float mResizeSquareDistance; 53 54 // keep track of the most recent key changes and their times so we can 55 // revert motion caused by clicking 56 private static final int KEY_CHANGE_HISTORY_SIZE = 10; 57 private static final long KEY_CHANGE_REVERT_TIME_MS = 100; 58 59 /** 60 * This listener reports high level actions that have occurred, such as 61 * text entry (from keys or voice) or the action button being pressed. 62 */ 63 public interface InputListener { 64 public static final int ENTRY_TYPE_STRING = 0; 65 public static final int ENTRY_TYPE_BACKSPACE = 1; 66 public static final int ENTRY_TYPE_SUGGESTION = 2; 67 public static final int ENTRY_TYPE_LEFT = 3; 68 public static final int ENTRY_TYPE_RIGHT = 4; 69 public static final int ENTRY_TYPE_ACTION = 5; 70 public static final int ENTRY_TYPE_VOICE = 6; 71 public static final int ENTRY_TYPE_DISMISS = 7; 72 public static final int ENTRY_TYPE_VOICE_DISMISS = 8; 73 74 /** 75 * Sent when the user has selected something that should affect the text 76 * field, such as entering a character, selecting the action, or 77 * finishing a voice action. 78 * 79 * @param type The type of key selected 80 * @param keyCode the key code of the key if applicable 81 * @param result The text entered if applicable 82 */ onEntry(int type, int keyCode, CharSequence result)83 public void onEntry(int type, int keyCode, CharSequence result); 84 } 85 86 private static final class KeyChange { 87 public long time; 88 public PointF position; 89 KeyChange(long time, PointF position)90 public KeyChange(long time, PointF position) { 91 this.time = time; 92 this.position = position; 93 } 94 } 95 96 private class DoubleClickDetector { 97 final long DOUBLE_CLICK_TIMEOUT_MS = 200; 98 long mFirstClickTime = 0; 99 boolean mFirstClickShiftLocked; 100 reset()101 public void reset() { 102 mFirstClickTime = 0; 103 } 104 addEvent(long currTime)105 public void addEvent(long currTime) { 106 if (currTime - mFirstClickTime > DOUBLE_CLICK_TIMEOUT_MS) { 107 mFirstClickTime = currTime; 108 mFirstClickShiftLocked = mContainer.isCapsLockOn(); 109 commitKey(); 110 } else { 111 mContainer.onShiftDoubleClick(mFirstClickShiftLocked); 112 reset(); 113 } 114 } 115 } 116 117 private DoubleClickDetector mDoubleClickDetector = new DoubleClickDetector(); 118 119 private View.OnLayoutChangeListener mOnLayoutChangeListener 120 = new View.OnLayoutChangeListener() { 121 122 @Override 123 public void onLayoutChange(View v, int left, int top, int right, int bottom, 124 int oldLeft, int oldTop, int oldRight, int oldBottom) { 125 int w = right - left; 126 int h = bottom - top; 127 int oldW = oldRight - oldLeft; 128 int oldH = oldBottom - oldTop; 129 if (w > 0 && h > 0) { 130 if (w != oldW || h != oldH) { 131 initInputView(); 132 } 133 } 134 } 135 }; 136 137 private InputMethodService mContext; 138 private InputListener mInputListener; 139 private LeanbackKeyboardContainer mContainer; 140 141 private LeanbackKeyboardContainer.KeyFocus mDownFocus = 142 new LeanbackKeyboardContainer.KeyFocus(); 143 private LeanbackKeyboardContainer.KeyFocus mTempFocus = 144 new LeanbackKeyboardContainer.KeyFocus(); 145 146 ArrayList<KeyChange> mKeyChangeHistory = new ArrayList<KeyChange>(KEY_CHANGE_HISTORY_SIZE + 1); 147 private PointF mTempPoint = new PointF(); 148 149 private boolean mKeyDownReceived = false; 150 private boolean mLongPressHandled = false; 151 private KeyFocus mKeyDownKeyFocus; 152 private int mMoveCount; 153 LeanbackKeyboardController(InputMethodService context, InputListener listener)154 public LeanbackKeyboardController(InputMethodService context, InputListener listener) { 155 this(context, listener, new LeanbackKeyboardContainer(context)); 156 } 157 LeanbackKeyboardController(InputMethodService context, InputListener listener, LeanbackKeyboardContainer container)158 LeanbackKeyboardController(InputMethodService context, InputListener listener, 159 LeanbackKeyboardContainer container) { 160 mContext = context; 161 mResizeSquareDistance = context.getResources().getDimension(R.dimen.resize_move_distance); 162 mResizeSquareDistance *= mResizeSquareDistance; 163 mInputListener = listener; 164 setKeyboardContainer(container); 165 mContainer.setVoiceListener(this); 166 mContainer.setDismissListener(this); 167 } 168 169 /** 170 * This method is called when we start the input at a NEW input field. 171 */ onStartInput(EditorInfo attribute)172 public void onStartInput(EditorInfo attribute) { 173 if (mContainer != null) { 174 mContainer.onStartInput(attribute); 175 initInputView(); 176 } 177 } 178 179 /** 180 * This method is called by whenever we bring up the IME at an input field. 181 */ onStartInputView()182 public void onStartInputView() { 183 mKeyDownReceived = false; 184 if (mContainer != null) { 185 mContainer.onStartInputView(); 186 } 187 mDoubleClickDetector.reset(); 188 } 189 190 /** 191 * This method sets the pixel positions in mSpaceTracker to match the 192 * current KeyFocus in {@link LeanbackKeyboardContainer} This method is called 193 * when the keyboard layout is complete, after 194 * {@link LeanbackKeyboardContainer.onInitInputView}, to initialize the starting 195 * position of mSpaceTracker; and in onUp to reset the pixel position in 196 * mSpaceTracker. 197 */ updatePositionToCurrentFocus()198 private void updatePositionToCurrentFocus() { 199 PointF currPosition = getCurrentKeyPosition(); 200 if (currPosition != null) { 201 } 202 } 203 initInputView()204 private void initInputView() { 205 mContainer.onInitInputView(); 206 updatePositionToCurrentFocus(); 207 } 208 getCurrentKeyPosition()209 private PointF getCurrentKeyPosition() { 210 if (mContainer != null) { 211 LeanbackKeyboardContainer.KeyFocus initialKeyInfo = mContainer.getCurrFocus(); 212 return new PointF(initialKeyInfo.rect.centerX(), initialKeyInfo.rect.centerY()); 213 } 214 return null; 215 } 216 performBestSnap(long time)217 private void performBestSnap(long time) { 218 KeyFocus focus = mContainer.getCurrFocus(); 219 mTempPoint.x = focus.rect.centerX(); 220 mTempPoint.y = focus.rect.centerY(); 221 PointF bestSnap = getBestSnapPosition(mTempPoint, time); 222 mContainer.getBestFocus(bestSnap.x, bestSnap.y, mTempFocus); 223 mContainer.setFocus(mTempFocus); 224 updatePositionToCurrentFocus(); 225 } 226 getBestSnapPosition(PointF currPoint, long currTime)227 private PointF getBestSnapPosition(PointF currPoint, long currTime) { 228 if (mKeyChangeHistory.size() <= 1) { 229 return currPoint; 230 } 231 for (int i = 0; i < mKeyChangeHistory.size() - 1; i++) { 232 KeyChange change = mKeyChangeHistory.get(i); 233 KeyChange nextChange = mKeyChangeHistory.get(i + 1); 234 if (currTime - nextChange.time < KEY_CHANGE_REVERT_TIME_MS) { 235 if (DEBUG) { 236 Log.d(TAG, "Reverting keychange to " + change.position.toString()); 237 } 238 // Return the oldest key change within the revert window and 239 // clear all key changes 240 currPoint = change.position; 241 // on a revert, clear the history and add the reverting point. 242 // This way the reverted point will be preferred if there's 243 // another fast change before the next call. 244 mKeyChangeHistory.clear(); 245 mKeyChangeHistory.add(new KeyChange(currTime, currPoint)); 246 break; 247 } 248 } 249 return currPoint; 250 } 251 setKeyboardContainer(LeanbackKeyboardContainer container)252 public void setKeyboardContainer(LeanbackKeyboardContainer container) { 253 mContainer = container; 254 container.getView().addOnLayoutChangeListener(mOnLayoutChangeListener); 255 } 256 getView()257 public View getView() { 258 if (mContainer != null) { 259 return mContainer.getView(); 260 } 261 return null; 262 } 263 areSuggestionsEnabled()264 public boolean areSuggestionsEnabled() { 265 if (mContainer != null) { 266 return mContainer.areSuggestionsEnabled(); 267 } 268 return false; 269 } 270 enableAutoEnterSpace()271 public boolean enableAutoEnterSpace() { 272 if (mContainer != null) { 273 return mContainer.enableAutoEnterSpace(); 274 } 275 return false; 276 } 277 onKeyDown(int keyCode, KeyEvent event)278 public boolean onKeyDown(int keyCode, KeyEvent event) { 279 mDownFocus.set(mContainer.getCurrFocus()); 280 // this will handle other events, e.g. hardware keyboard 281 if (isEnterKey(keyCode)) { 282 mKeyDownReceived = true; 283 // first keyDown 284 if (event.getRepeatCount() == 0) { 285 mContainer.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_CLICK); 286 } 287 } 288 289 return handleKeyDownEvent(keyCode, event.getRepeatCount()); 290 } 291 onKeyUp(int keyCode, KeyEvent event)292 public boolean onKeyUp(int keyCode, KeyEvent event) { 293 // this only handles InputDevice.SOURCE_TOUCH_NAVIGATION events 294 if (isEnterKey(keyCode)) { 295 if (!mKeyDownReceived || mLongPressHandled) { 296 mLongPressHandled = false; 297 return true; 298 } 299 mKeyDownReceived = false; 300 if (mContainer.getTouchState() == LeanbackKeyboardContainer.TOUCH_STATE_CLICK) { 301 mContainer.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_TOUCH_SNAP); 302 } 303 } 304 return handleKeyUpEvent(keyCode, event.getEventTime()); 305 } 306 onGenericMotionEvent(MotionEvent event)307 public boolean onGenericMotionEvent(MotionEvent event) { 308 return false; 309 } 310 onDirectionalMove(int dir)311 private boolean onDirectionalMove(int dir) { 312 if (mContainer.getNextFocusInDirection(dir, mDownFocus, mTempFocus)) { 313 mContainer.setFocus(mTempFocus); 314 mDownFocus.set(mTempFocus); 315 316 clearKeyIfNecessary(); 317 } 318 319 return true; 320 } 321 clearKeyIfNecessary()322 private void clearKeyIfNecessary() { 323 mMoveCount++; 324 if (mMoveCount >= 3) { 325 mMoveCount = 0; 326 mKeyDownKeyFocus = null; 327 } 328 } 329 commitKey()330 private void commitKey() { 331 commitKey(mContainer.getCurrFocus()); 332 } 333 commitKey(LeanbackKeyboardContainer.KeyFocus keyFocus)334 private void commitKey(LeanbackKeyboardContainer.KeyFocus keyFocus) { 335 if (mContainer == null || keyFocus == null) { 336 return; 337 } 338 339 switch (keyFocus.type) { 340 case KeyFocus.TYPE_VOICE: 341 // voice doesn't have to go through the IME 342 mContainer.onVoiceClick(); 343 break; 344 case KeyFocus.TYPE_ACTION: 345 mInputListener.onEntry(InputListener.ENTRY_TYPE_ACTION, 0, null); 346 break; 347 case KeyFocus.TYPE_SUGGESTION: 348 mInputListener.onEntry(InputListener.ENTRY_TYPE_SUGGESTION, 0, 349 mContainer.getSuggestionText(keyFocus.index)); 350 break; 351 default: 352 Key key = mContainer.getKey(keyFocus.type, keyFocus.index); 353 if (key != null) { 354 int code = key.codes[0]; 355 CharSequence label = key.label; 356 handleCommitKeyboardKey(code, label); 357 } 358 break; 359 360 } 361 } 362 handleCommitKeyboardKey(int code, CharSequence label)363 private void handleCommitKeyboardKey(int code, CharSequence label) { 364 switch (code) { 365 case Keyboard.KEYCODE_MODE_CHANGE: 366 if (Log.isLoggable(TAG, Log.VERBOSE)) { 367 Log.d(TAG, "mode change"); 368 } 369 mContainer.onModeChangeClick(); 370 break; 371 case LeanbackKeyboardView.KEYCODE_CAPS_LOCK: 372 mContainer.onShiftDoubleClick(mContainer.isCapsLockOn()); 373 break; 374 case Keyboard.KEYCODE_SHIFT: 375 // TODO invalidate and draw a different shift 376 // key in the function keyboard 377 if (Log.isLoggable(TAG, Log.VERBOSE)) { 378 Log.d(TAG, "shift"); 379 } 380 mContainer.onShiftClick(); 381 break; 382 case LeanbackKeyboardView.KEYCODE_DISMISS_MINI_KEYBOARD: 383 mContainer.dismissMiniKeyboard(); 384 break; 385 case LeanbackKeyboardView.KEYCODE_LEFT: 386 mInputListener.onEntry(InputListener.ENTRY_TYPE_LEFT, 0, null); 387 break; 388 case LeanbackKeyboardView.KEYCODE_RIGHT: 389 mInputListener.onEntry(InputListener.ENTRY_TYPE_RIGHT, 0, null); 390 break; 391 case Keyboard.KEYCODE_DELETE: 392 mInputListener.onEntry(InputListener.ENTRY_TYPE_BACKSPACE, 0, null); 393 break; 394 case LeanbackKeyboardView.ASCII_SPACE: 395 mInputListener.onEntry(InputListener.ENTRY_TYPE_STRING, code, " "); 396 mContainer.onSpaceEntry(); 397 break; 398 case LeanbackKeyboardView.ASCII_PERIOD: 399 mInputListener.onEntry(InputListener.ENTRY_TYPE_STRING, code, label); 400 mContainer.onPeriodEntry(); 401 break; 402 case LeanbackKeyboardView.KEYCODE_VOICE: 403 mContainer.startVoiceRecording(); 404 break; 405 // fall through to default with this label 406 default: 407 mInputListener.onEntry(InputListener.ENTRY_TYPE_STRING, code, label); 408 mContainer.onTextEntry(); 409 410 if (mContainer.isMiniKeyboardOnScreen()) { 411 mContainer.dismissMiniKeyboard(); 412 } 413 break; 414 } 415 } 416 handleKeyDownEvent(int keyCode, int eventRepeatCount)417 private boolean handleKeyDownEvent(int keyCode, int eventRepeatCount) { 418 keyCode = getSimplifiedKey(keyCode); 419 420 // never trap back 421 if (keyCode == KeyEvent.KEYCODE_BACK) { 422 mContainer.cancelVoiceRecording(); 423 return false; 424 } 425 426 // capture all key downs when voice is visible 427 if (mContainer.isVoiceVisible()) { 428 if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 429 mContainer.cancelVoiceRecording(); 430 } 431 return true; 432 } 433 434 boolean handled = true; 435 switch(keyCode) { 436 // Direction keys are handled on down to allow repeated movement 437 case KeyEvent.KEYCODE_DPAD_LEFT: 438 handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_LEFT); 439 break; 440 case KeyEvent.KEYCODE_DPAD_RIGHT: 441 handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_RIGHT); 442 break; 443 case KeyEvent.KEYCODE_DPAD_UP: 444 handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_UP); 445 break; 446 case KeyEvent.KEYCODE_DPAD_DOWN: 447 handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_DOWN); 448 break; 449 case KeyEvent.KEYCODE_BUTTON_X: 450 handleCommitKeyboardKey(Keyboard.KEYCODE_DELETE, null); 451 break; 452 case KeyEvent.KEYCODE_BUTTON_Y: 453 handleCommitKeyboardKey(LeanbackKeyboardView.ASCII_SPACE, null); 454 break; 455 case KeyEvent.KEYCODE_BUTTON_L1: 456 handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_LEFT, null); 457 break; 458 case KeyEvent.KEYCODE_BUTTON_R1: 459 handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_RIGHT, null); 460 break; 461 // these are handled on up 462 case KeyEvent.KEYCODE_DPAD_CENTER: 463 if (eventRepeatCount == 0) { 464 mMoveCount = 0; 465 mKeyDownKeyFocus = new KeyFocus(); 466 mKeyDownKeyFocus.set(mContainer.getCurrFocus()); 467 } else if (eventRepeatCount == 1) { 468 if (handleKeyLongPress(keyCode)) { 469 mKeyDownKeyFocus = null; 470 } 471 } 472 473 if (isKeyHandledOnKeyDown(mContainer.getCurrKeyCode())) { 474 commitKey(); 475 } 476 break; 477 // also handled on up 478 case KeyEvent.KEYCODE_BUTTON_THUMBL: 479 case KeyEvent.KEYCODE_BUTTON_THUMBR: 480 case KeyEvent.KEYCODE_ENTER: 481 break; 482 default: 483 handled = false; 484 break; 485 } 486 return handled; 487 } 488 handleKeyLongPress(int keyCode)489 private boolean handleKeyLongPress(int keyCode) { 490 mLongPressHandled = isEnterKey(keyCode) && mContainer.onKeyLongPress(); 491 if (mContainer.isMiniKeyboardOnScreen()) { 492 Log.d(TAG, "mini keyboard shown after long press"); 493 } 494 return mLongPressHandled; 495 } 496 isKeyHandledOnKeyDown(int currKeyCode)497 private boolean isKeyHandledOnKeyDown(int currKeyCode) { 498 return currKeyCode == Keyboard.KEYCODE_DELETE 499 || currKeyCode == LeanbackKeyboardView.KEYCODE_LEFT 500 || currKeyCode == LeanbackKeyboardView.KEYCODE_RIGHT; 501 } 502 503 /** 504 * This handles all key events from an input device 505 * @param keyCode 506 * @return true if the key was handled, false otherwise 507 */ handleKeyUpEvent(int keyCode, long currTime)508 private boolean handleKeyUpEvent(int keyCode, long currTime) { 509 keyCode = getSimplifiedKey(keyCode); 510 511 // never trap back 512 if (keyCode == KeyEvent.KEYCODE_BACK) { 513 return false; 514 } 515 516 // capture all key ups when voice is visible 517 if (mContainer.isVoiceVisible()) { 518 return true; 519 } 520 521 boolean handled = true; 522 switch(keyCode) { 523 // Some keys are handled on down to allow repeats 524 case KeyEvent.KEYCODE_DPAD_LEFT: 525 case KeyEvent.KEYCODE_DPAD_RIGHT: 526 case KeyEvent.KEYCODE_DPAD_UP: 527 case KeyEvent.KEYCODE_DPAD_DOWN: 528 clearKeyIfNecessary(); 529 break; 530 case KeyEvent.KEYCODE_BUTTON_X: 531 case KeyEvent.KEYCODE_BUTTON_Y: 532 case KeyEvent.KEYCODE_BUTTON_L1: 533 case KeyEvent.KEYCODE_BUTTON_R1: 534 break; 535 case KeyEvent.KEYCODE_DPAD_CENTER: 536 if (mContainer.getCurrKeyCode() == Keyboard.KEYCODE_SHIFT) { 537 mDoubleClickDetector.addEvent(currTime); 538 } else if (!isKeyHandledOnKeyDown(mContainer.getCurrKeyCode())) { 539 commitKey(mKeyDownKeyFocus); 540 } 541 break; 542 case KeyEvent.KEYCODE_BUTTON_THUMBL: 543 handleCommitKeyboardKey(Keyboard.KEYCODE_MODE_CHANGE, null); 544 break; 545 case KeyEvent.KEYCODE_BUTTON_THUMBR: 546 handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_CAPS_LOCK, null); 547 break; 548 case KeyEvent.KEYCODE_ENTER: 549 if (mContainer != null) { 550 KeyFocus keyFocus = mContainer.getCurrFocus(); 551 if (keyFocus != null && keyFocus.type == KeyFocus.TYPE_SUGGESTION) { 552 mInputListener.onEntry(InputListener.ENTRY_TYPE_SUGGESTION, 0, 553 mContainer.getSuggestionText(keyFocus.index)); 554 } 555 } 556 mInputListener.onEntry(InputListener.ENTRY_TYPE_DISMISS, 0, null); 557 break; 558 default: 559 handled = false; 560 break; 561 } 562 return handled; 563 } 564 updateSuggestions(ArrayList<String> suggestions)565 public void updateSuggestions(ArrayList<String> suggestions) { 566 if (mContainer != null) { 567 mContainer.updateSuggestions(suggestions); 568 } 569 } 570 571 @Override onVoiceResult(String result)572 public void onVoiceResult(String result) { 573 mInputListener.onEntry(InputListener.ENTRY_TYPE_VOICE, 0, result); 574 } 575 576 @Override onDismiss(boolean fromVoice)577 public void onDismiss(boolean fromVoice) { 578 if (fromVoice) { 579 mInputListener.onEntry(InputListener.ENTRY_TYPE_VOICE_DISMISS, 0, null); 580 } else { 581 mInputListener.onEntry(InputListener.ENTRY_TYPE_DISMISS, 0, null); 582 } 583 } 584 isEnterKey(int keyCode)585 private boolean isEnterKey(int keyCode) { 586 return getSimplifiedKey(keyCode) == KeyEvent.KEYCODE_DPAD_CENTER; 587 } 588 getSimplifiedKey(int keyCode)589 private int getSimplifiedKey(int keyCode) { 590 // simplify for dpad center 591 keyCode = (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || 592 keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER || 593 keyCode == KeyEvent.KEYCODE_BUTTON_A) ? KeyEvent.KEYCODE_DPAD_CENTER : keyCode; 594 595 // simply for back 596 keyCode = (keyCode == KeyEvent.KEYCODE_BUTTON_B ? KeyEvent.KEYCODE_BACK : keyCode); 597 598 return keyCode; 599 } 600 } 601