1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.geckoview; 7 8 import android.annotation.TargetApi; 9 import android.content.Context; 10 import android.content.res.Configuration; 11 import android.graphics.Matrix; 12 import android.graphics.RectF; 13 import android.media.AudioManager; 14 import android.os.Build; 15 import android.os.Handler; 16 import android.os.Looper; 17 import androidx.annotation.NonNull; 18 import android.text.Editable; 19 import android.text.Selection; 20 import android.text.SpannableString; 21 import android.util.DisplayMetrics; 22 import android.util.Log; 23 import android.view.KeyEvent; 24 import android.view.View; 25 import android.view.inputmethod.BaseInputConnection; 26 import android.view.inputmethod.CursorAnchorInfo; 27 import android.view.inputmethod.EditorInfo; 28 import android.view.inputmethod.ExtractedText; 29 import android.view.inputmethod.ExtractedTextRequest; 30 import android.view.inputmethod.InputConnection; 31 32 import org.mozilla.gecko.Clipboard; 33 import org.mozilla.gecko.InputMethods; 34 import org.mozilla.gecko.util.ThreadUtils; 35 36 import java.lang.reflect.InvocationHandler; 37 import java.lang.reflect.Method; 38 import java.lang.reflect.Proxy; 39 40 /* package */ final class GeckoInputConnection 41 extends BaseInputConnection 42 implements SessionTextInput.InputConnectionClient, 43 SessionTextInput.EditableListener { 44 45 private static final boolean DEBUG = false; 46 protected static final String LOGTAG = "GeckoInputConnection"; 47 48 private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection"; 49 private static final String CUSTOM_HANDLER_TEST_CLASS = 50 "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput"; 51 52 private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480; 53 54 private static Handler sBackgroundHandler; 55 56 // Managed only by notifyIMEContext; see comments in notifyIMEContext 57 private int mIMEState; 58 private String mIMEActionHint = ""; 59 private int mLastSelectionStart; 60 private int mLastSelectionEnd; 61 62 private String mCurrentInputMethod = ""; 63 64 private final GeckoSession mSession; 65 private final View mView; 66 private final SessionTextInput.EditableClient mEditableClient; 67 protected int mBatchEditCount; 68 private ExtractedTextRequest mUpdateRequest; 69 private final InputConnection mKeyInputConnection; 70 private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder; 71 create( final GeckoSession session, final View targetView, final SessionTextInput.EditableClient editable)72 public static SessionTextInput.InputConnectionClient create( 73 final GeckoSession session, 74 final View targetView, 75 final SessionTextInput.EditableClient editable) { 76 SessionTextInput.InputConnectionClient ic = new GeckoInputConnection(session, targetView, editable); 77 if (DEBUG) { 78 ic = wrapForDebug(ic); 79 } 80 return ic; 81 } 82 wrapForDebug(final SessionTextInput.InputConnectionClient ic)83 private static SessionTextInput.InputConnectionClient wrapForDebug(final SessionTextInput.InputConnectionClient ic) { 84 final InvocationHandler handler = new InvocationHandler() { 85 private final StringBuilder mCallLevel = new StringBuilder(); 86 87 @Override 88 public Object invoke(final Object proxy, final Method method, 89 final Object[] args) throws Throwable { 90 final StringBuilder log = new StringBuilder(mCallLevel); 91 log.append("> ").append(method.getName()).append("("); 92 if (args != null) { 93 for (int i = 0; i < args.length; i++) { 94 final Object arg = args[i]; 95 // translate argument values to constant names 96 if ("notifyIME".equals(method.getName()) && i == 0) { 97 log.append(GeckoEditable.getConstantName( 98 SessionTextInput.EditableListener.class, 99 "NOTIFY_IME_", arg)); 100 } else if ("notifyIMEContext".equals(method.getName()) && i == 0) { 101 log.append(GeckoEditable.getConstantName( 102 SessionTextInput.EditableListener.class, 103 "IME_STATE_", arg)); 104 } else { 105 GeckoEditable.debugAppend(log, arg); 106 } 107 log.append(", "); 108 } 109 if (args.length > 0) { 110 log.setLength(log.length() - 2); 111 } 112 } 113 log.append(")"); 114 Log.d(LOGTAG, log.toString()); 115 116 mCallLevel.append(' '); 117 Object ret = method.invoke(ic, args); 118 if (ret == ic) { 119 ret = proxy; 120 } 121 mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1)); 122 123 log.setLength(mCallLevel.length()); 124 log.append("< ").append(method.getName()); 125 if (!method.getReturnType().equals(Void.TYPE)) { 126 GeckoEditable.debugAppend(log.append(": "), ret); 127 } 128 Log.d(LOGTAG, log.toString()); 129 return ret; 130 } 131 }; 132 133 return (SessionTextInput.InputConnectionClient) Proxy.newProxyInstance( 134 GeckoInputConnection.class.getClassLoader(), 135 new Class<?>[] { 136 InputConnection.class, 137 SessionTextInput.InputConnectionClient.class, 138 SessionTextInput.EditableListener.class 139 }, handler); 140 } 141 GeckoInputConnection(final GeckoSession session, final View targetView, final SessionTextInput.EditableClient editable)142 protected GeckoInputConnection(final GeckoSession session, 143 final View targetView, 144 final SessionTextInput.EditableClient editable) { 145 super(targetView, true); 146 mSession = session; 147 mView = targetView; 148 mEditableClient = editable; 149 mIMEState = IME_STATE_DISABLED; 150 // InputConnection that sends keys for plugins, which don't have full editors 151 mKeyInputConnection = new BaseInputConnection(targetView, false); 152 } 153 154 @Override beginBatchEdit()155 public synchronized boolean beginBatchEdit() { 156 mBatchEditCount++; 157 if (mBatchEditCount == 1) { 158 mEditableClient.setBatchMode(true); 159 } 160 return true; 161 } 162 163 @Override endBatchEdit()164 public synchronized boolean endBatchEdit() { 165 if (mBatchEditCount <= 0) { 166 Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount <= 0?!"); 167 return true; 168 } 169 170 mBatchEditCount--; 171 if (mBatchEditCount != 0) { 172 return true; 173 } 174 175 // setBatchMode will call onTextChange and/or onSelectionChange for us. 176 mEditableClient.setBatchMode(false); 177 return true; 178 } 179 180 @Override getEditable()181 public Editable getEditable() { 182 return mEditableClient.getEditable(); 183 } 184 185 @Override performContextMenuAction(final int id)186 public boolean performContextMenuAction(final int id) { 187 final View view = getView(); 188 final Editable editable = getEditable(); 189 if (view == null || editable == null) { 190 return false; 191 } 192 final int selStart = Selection.getSelectionStart(editable); 193 final int selEnd = Selection.getSelectionEnd(editable); 194 195 switch (id) { 196 case android.R.id.selectAll: 197 setSelection(0, editable.length()); 198 break; 199 case android.R.id.cut: 200 // If selection is empty, we'll select everything 201 if (selStart == selEnd) { 202 // Fill the clipboard 203 Clipboard.setText(view.getContext(), editable); 204 editable.clear(); 205 } else { 206 Clipboard.setText(view.getContext(), 207 editable.subSequence(Math.min(selStart, selEnd), 208 Math.max(selStart, selEnd))); 209 editable.delete(selStart, selEnd); 210 } 211 break; 212 case android.R.id.paste: 213 final String text = Clipboard.getText(view.getContext()); 214 if (text != null) { 215 commitText(text, 1); 216 } 217 break; 218 case android.R.id.copy: 219 // Copy the current selection or the empty string if nothing is selected. 220 final String copiedText = selStart == selEnd ? "" : 221 editable.toString().substring( 222 Math.min(selStart, selEnd), 223 Math.max(selStart, selEnd)); 224 Clipboard.setText(view.getContext(), copiedText); 225 break; 226 } 227 return true; 228 } 229 230 @Override performEditorAction(final int editorAction)231 public boolean performEditorAction(final int editorAction) { 232 if (editorAction == EditorInfo.IME_ACTION_PREVIOUS && 233 !mIMEActionHint.equals("previous")) { 234 // This action is [Previous] key on FireTV's keyboard. 235 // [Previous] closes software keyboard, and don't generate any keyboard event. 236 getView().post(new Runnable() { 237 @Override 238 public void run() { 239 getInputDelegate().hideSoftInput(mSession); 240 } 241 }); 242 return true; 243 } 244 return super.performEditorAction(editorAction); 245 } 246 247 @Override getExtractedText(final ExtractedTextRequest req, final int flags)248 public ExtractedText getExtractedText(final ExtractedTextRequest req, final int flags) { 249 if (req == null) 250 return null; 251 252 if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) 253 mUpdateRequest = req; 254 255 final Editable editable = getEditable(); 256 if (editable == null) { 257 return null; 258 } 259 final int selStart = Selection.getSelectionStart(editable); 260 final int selEnd = Selection.getSelectionEnd(editable); 261 262 final ExtractedText extract = new ExtractedText(); 263 extract.flags = 0; 264 extract.partialStartOffset = -1; 265 extract.partialEndOffset = -1; 266 extract.selectionStart = selStart; 267 extract.selectionEnd = selEnd; 268 extract.startOffset = 0; 269 if ((req.flags & GET_TEXT_WITH_STYLES) != 0) { 270 extract.text = new SpannableString(editable); 271 } else { 272 extract.text = editable.toString(); 273 } 274 return extract; 275 } 276 277 @Override // SessionTextInput.InputConnectionClient getView()278 public View getView() { 279 return mView; 280 } 281 282 @NonNull getInputDelegate()283 /* package */ GeckoSession.TextInputDelegate getInputDelegate() { 284 return mSession.getTextInput().getDelegate(); 285 } 286 287 @Override // SessionTextInput.EditableListener onTextChange()288 public void onTextChange() { 289 final Editable editable = getEditable(); 290 if (mUpdateRequest == null || editable == null) { 291 return; 292 } 293 294 final ExtractedTextRequest request = mUpdateRequest; 295 final ExtractedText extractedText = new ExtractedText(); 296 extractedText.flags = 0; 297 // Update the entire Editable range 298 extractedText.partialStartOffset = -1; 299 extractedText.partialEndOffset = -1; 300 extractedText.selectionStart = Selection.getSelectionStart(editable); 301 extractedText.selectionEnd = Selection.getSelectionEnd(editable); 302 extractedText.startOffset = 0; 303 if ((request.flags & GET_TEXT_WITH_STYLES) != 0) { 304 extractedText.text = new SpannableString(editable); 305 } else { 306 extractedText.text = editable.toString(); 307 } 308 309 getView().post(new Runnable() { 310 @Override 311 public void run() { 312 getInputDelegate().updateExtractedText(mSession, request, extractedText); 313 } 314 }); 315 } 316 317 @Override // SessionTextInput.EditableListener onSelectionChange()318 public void onSelectionChange() { 319 320 final Editable editable = getEditable(); 321 if (editable != null) { 322 mLastSelectionStart = Selection.getSelectionStart(editable); 323 mLastSelectionEnd = Selection.getSelectionEnd(editable); 324 notifySelectionChange(mLastSelectionStart, mLastSelectionEnd); 325 } 326 } 327 notifySelectionChange(final int start, final int end)328 private void notifySelectionChange(final int start, final int end) { 329 final Editable editable = getEditable(); 330 if (editable == null) { 331 return; 332 } 333 334 final int compositionStart = getComposingSpanStart(editable); 335 final int compositionEnd = getComposingSpanEnd(editable); 336 337 getView().post(new Runnable() { 338 @Override 339 public void run() { 340 getInputDelegate().updateSelection(mSession, start, end, 341 compositionStart, compositionEnd); 342 } 343 }); 344 } 345 346 @Override // SessionTextInput.EditableListener onDiscardComposition()347 public void onDiscardComposition() { 348 final View view = getView(); 349 if (view == null) { 350 return; 351 } 352 353 // InputMethodManager.updateSelection will remove composition 354 // on most IMEs. But ATOK series do nothing. So we have to 355 // restart input method to remove composition as workaround. 356 if (!InputMethods.needsRestartInput(InputMethods.getCurrentInputMethod(view.getContext()))) { 357 return; 358 } 359 360 view.post(new Runnable() { 361 @Override 362 public void run() { 363 getInputDelegate().restartInput(mSession, GeckoSession.TextInputDelegate.RESTART_REASON_CONTENT_CHANGE); 364 } 365 }); 366 } 367 368 @TargetApi(21) 369 @Override // SessionTextInput.EditableListener updateCompositionRects(final RectF[] rects)370 public void updateCompositionRects(final RectF[] rects) { 371 if (!(Build.VERSION.SDK_INT >= 21)) { 372 return; 373 } 374 375 final View view = getView(); 376 if (view == null) { 377 return; 378 } 379 380 final Editable content = getEditable(); 381 if (content == null) { 382 return; 383 } 384 385 final int composingStart = getComposingSpanStart(content); 386 final int composingEnd = getComposingSpanEnd(content); 387 if (composingStart < 0 || composingEnd < 0) { 388 if (DEBUG) { 389 Log.d(LOGTAG, "No composition for updates"); 390 } 391 return; 392 } 393 394 final CharSequence composition = content.subSequence(composingStart, composingEnd); 395 396 view.post(new Runnable() { 397 @Override 398 public void run() { 399 updateCompositionRectsOnUi(view, rects, composition); 400 } 401 }); 402 } 403 404 @TargetApi(21) updateCompositionRectsOnUi(final View view, final RectF[] rects, final CharSequence composition)405 /* package */ void updateCompositionRectsOnUi(final View view, 406 final RectF[] rects, 407 final CharSequence composition) { 408 if (mCursorAnchorInfoBuilder == null) { 409 mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder(); 410 } 411 mCursorAnchorInfoBuilder.reset(); 412 413 final Matrix matrix = new Matrix(); 414 mSession.getClientToScreenMatrix(matrix); 415 mCursorAnchorInfoBuilder.setMatrix(matrix); 416 417 for (int i = 0; i < rects.length; i++) { 418 mCursorAnchorInfoBuilder.addCharacterBounds( 419 i, rects[i].left, rects[i].top, rects[i].right, rects[i].bottom, 420 CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION); 421 } 422 423 mCursorAnchorInfoBuilder.setComposingText(0, composition); 424 425 final CursorAnchorInfo info = mCursorAnchorInfoBuilder.build(); 426 getView().post(new Runnable() { 427 @Override 428 public void run() { 429 getInputDelegate().updateCursorAnchorInfo(mSession, info); 430 } 431 }); 432 } 433 434 @Override requestCursorUpdates(final int cursorUpdateMode)435 public boolean requestCursorUpdates(final int cursorUpdateMode) { 436 437 if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) { 438 mEditableClient.requestCursorUpdates( 439 SessionTextInput.EditableClient.ONE_SHOT); 440 } 441 442 if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0) { 443 mEditableClient.requestCursorUpdates( 444 SessionTextInput.EditableClient.START_MONITOR); 445 } else { 446 mEditableClient.requestCursorUpdates( 447 SessionTextInput.EditableClient.END_MONITOR); 448 } 449 return true; 450 } 451 452 @Override // SessionTextInput.EditableListener onDefaultKeyEvent(final KeyEvent event)453 public void onDefaultKeyEvent(final KeyEvent event) { 454 ThreadUtils.runOnUiThread(new Runnable() { 455 @Override 456 public void run() { 457 GeckoInputConnection.this.performDefaultKeyAction(event); 458 } 459 }); 460 } 461 getBackgroundHandler()462 private static synchronized Handler getBackgroundHandler() { 463 if (sBackgroundHandler != null) { 464 return sBackgroundHandler; 465 } 466 // Don't use GeckoBackgroundThread because Gecko thread may block waiting on 467 // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME, 468 // GeckoBackgroundThread may end up also block waiting on Gecko thread and a 469 // deadlock occurs 470 final Thread backgroundThread = new Thread(new Runnable() { 471 @Override 472 public void run() { 473 Looper.prepare(); 474 synchronized (GeckoInputConnection.class) { 475 sBackgroundHandler = new Handler(); 476 GeckoInputConnection.class.notify(); 477 } 478 Looper.loop(); 479 // We should never be exiting the thread loop. 480 throw new IllegalThreadStateException("unreachable code"); 481 } 482 }, LOGTAG); 483 backgroundThread.setDaemon(true); 484 backgroundThread.start(); 485 while (sBackgroundHandler == null) { 486 try { 487 // wait for new thread to set sBackgroundHandler 488 GeckoInputConnection.class.wait(); 489 } catch (final InterruptedException e) { 490 } 491 } 492 return sBackgroundHandler; 493 } 494 canReturnCustomHandler()495 private synchronized boolean canReturnCustomHandler() { 496 if (mIMEState == IME_STATE_DISABLED) { 497 return false; 498 } 499 for (final StackTraceElement frame : Thread.currentThread().getStackTrace()) { 500 // We only return our custom Handler to InputMethodManager's InputConnection 501 // proxy. For all other purposes, we return the regular Handler. 502 // InputMethodManager retrieves the Handler for its InputConnection proxy 503 // inside its method startInputInner(), so we check for that here. This is 504 // valid from Android 2.2 to at least Android 4.2. If this situation ever 505 // changes, we gracefully fall back to using the regular Handler. 506 if ("startInputInner".equals(frame.getMethodName()) && 507 "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) { 508 // Only return our own Handler to InputMethodManager and only prior to 24. 509 return Build.VERSION.SDK_INT < 24; 510 } 511 if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) && 512 CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) { 513 // InputConnection tests should also run on the custom handler 514 return true; 515 } 516 } 517 return false; 518 } 519 isPhysicalKeyboardPresent()520 private boolean isPhysicalKeyboardPresent() { 521 final View v = getView(); 522 if (v == null) { 523 return false; 524 } 525 final Configuration config = v.getContext().getResources().getConfiguration(); 526 return config.keyboard != Configuration.KEYBOARD_NOKEYS; 527 } 528 529 @Override // InputConnection getHandler()530 public Handler getHandler() { 531 final Handler handler; 532 if (isPhysicalKeyboardPresent()) { 533 handler = ThreadUtils.getUiHandler(); 534 } else { 535 handler = getBackgroundHandler(); 536 } 537 return mEditableClient.setInputConnectionHandler(handler); 538 } 539 540 @Override // SessionTextInput.InputConnectionClient getHandler(final Handler defHandler)541 public Handler getHandler(final Handler defHandler) { 542 if (!canReturnCustomHandler()) { 543 return defHandler; 544 } 545 546 return getHandler(); 547 } 548 549 @Override // InputConnection closeConnection()550 public void closeConnection() { 551 if (mBatchEditCount != 0) { 552 // GBoard may call this into batch edit mode then it doesn't call endBatchEdit. 553 // Since we are recycle GeckoInputConnection, we have to reset 554 // batch count even if IME/keyboard bug. 555 if (DEBUG) { 556 Log.d(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount); 557 } 558 mBatchEditCount = 0; 559 // setBatchMode will call onTextChange and/or onSelectionChange for us. 560 mEditableClient.setBatchMode(false); 561 } 562 super.closeConnection(); 563 } 564 565 @Override // SessionTextInput.InputConnectionClient onCreateInputConnection(final EditorInfo outAttrs)566 public synchronized InputConnection onCreateInputConnection(final EditorInfo outAttrs) { 567 if (mIMEState == IME_STATE_DISABLED) { 568 return null; 569 } 570 571 final Context context = getView().getContext(); 572 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 573 if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) { 574 // prevent showing full-screen keyboard only when the screen is tall enough 575 // to show some reasonable amount of the page (see bug 752709) 576 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI 577 | EditorInfo.IME_FLAG_NO_FULLSCREEN; 578 } 579 580 if (DEBUG) { 581 Log.d(LOGTAG, "mapped IME states to: inputType = " + 582 Integer.toHexString(outAttrs.inputType) + ", imeOptions = " + 583 Integer.toHexString(outAttrs.imeOptions)); 584 } 585 586 final String prevInputMethod = mCurrentInputMethod; 587 mCurrentInputMethod = InputMethods.getCurrentInputMethod(context); 588 if (DEBUG) { 589 Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod); 590 } 591 592 outAttrs.initialSelStart = mLastSelectionStart; 593 outAttrs.initialSelEnd = mLastSelectionEnd; 594 return this; 595 } 596 replaceComposingSpanWithSelection()597 private boolean replaceComposingSpanWithSelection() { 598 final Editable content = getEditable(); 599 if (content == null) { 600 return false; 601 } 602 final int a = getComposingSpanStart(content); 603 final int b = getComposingSpanEnd(content); 604 if (a != -1 && b != -1) { 605 if (DEBUG) { 606 Log.d(LOGTAG, "removing composition at " + a + "-" + b); 607 } 608 removeComposingSpans(content); 609 Selection.setSelection(content, a, b); 610 } 611 return true; 612 } 613 614 @Override commitText(final CharSequence text, final int newCursorPosition)615 public boolean commitText(final CharSequence text, final int newCursorPosition) { 616 if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) && 617 text.length() == 1 && newCursorPosition > 0) { 618 if (DEBUG) { 619 Log.d(LOGTAG, "committing \"" + text + "\" as key"); 620 } 621 // mKeyInputConnection is a BaseInputConnection that commits text as keys; 622 // but we first need to replace any composing span with a selection, 623 // so that the new key events will generate characters to replace 624 // text from the old composing span 625 return replaceComposingSpanWithSelection() && 626 mKeyInputConnection.commitText(text, newCursorPosition); 627 } 628 return super.commitText(text, newCursorPosition); 629 } 630 631 @Override setSelection(final int start, final int end)632 public boolean setSelection(final int start, final int end) { 633 if (start < 0 || end < 0) { 634 // Some keyboards (e.g. Samsung) can call setSelection with 635 // negative offsets. In that case we ignore the call, similar to how 636 // BaseInputConnection.setSelection ignores offsets that go past the length. 637 return true; 638 } 639 return super.setSelection(start, end); 640 } 641 642 @Override sendKeyEvent(final @NonNull KeyEvent event)643 public boolean sendKeyEvent(final @NonNull KeyEvent event) { 644 final KeyEvent translatedEvent = translateKey(event.getKeyCode(), event); 645 mEditableClient.sendKeyEvent(getView(), event.getAction(), translatedEvent); 646 return false; // seems to always return false 647 } 648 translateKey(final int keyCode, final @NonNull KeyEvent event)649 private KeyEvent translateKey(final int keyCode, final @NonNull KeyEvent event) { 650 switch (keyCode) { 651 case KeyEvent.KEYCODE_ENTER: 652 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 && 653 mIMEActionHint.equals("maybenext")) { 654 // XXX It is not good to dispatch tab key for web compatibility. 655 // See https://github.com/w3c/uievents/issues/253 and bug 1600540. 656 return new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), KeyEvent.KEYCODE_TAB, 0); 657 } 658 break; 659 } 660 return event; 661 } 662 663 // Called by OnDefaultKeyEvent handler, up from Gecko performDefaultKeyAction(final KeyEvent event)664 /* package */ void performDefaultKeyAction(final KeyEvent event) { 665 switch (event.getKeyCode()) { 666 case KeyEvent.KEYCODE_MUTE: 667 case KeyEvent.KEYCODE_HEADSETHOOK: 668 case KeyEvent.KEYCODE_MEDIA_PLAY: 669 case KeyEvent.KEYCODE_MEDIA_PAUSE: 670 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: 671 case KeyEvent.KEYCODE_MEDIA_STOP: 672 case KeyEvent.KEYCODE_MEDIA_NEXT: 673 case KeyEvent.KEYCODE_MEDIA_PREVIOUS: 674 case KeyEvent.KEYCODE_MEDIA_REWIND: 675 case KeyEvent.KEYCODE_MEDIA_RECORD: 676 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: 677 case KeyEvent.KEYCODE_MEDIA_CLOSE: 678 case KeyEvent.KEYCODE_MEDIA_EJECT: 679 case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: 680 // Forward media keypresses to the registered handler so headset controls work 681 // Does the same thing as Chromium 682 // https://chromium.googlesource.com/chromium/src/+/49.0.2623.67/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsDelegateAndroid.java#445 683 // These are all the keys dispatchMediaKeyEvent supports. 684 if (Build.VERSION.SDK_INT >= 19) { 685 // dispatchMediaKeyEvent is only available on Android 4.4+ 686 final Context viewContext = getView().getContext(); 687 final AudioManager am = (AudioManager)viewContext.getSystemService(Context.AUDIO_SERVICE); 688 am.dispatchMediaKeyEvent(event); 689 } 690 break; 691 } 692 } 693 694 @Override // SessionTextInput.EditableListener notifyIME(final int type)695 public void notifyIME(final int type) { 696 switch (type) { 697 case NOTIFY_IME_OF_FOCUS: 698 // Showing/hiding vkb is done in notifyIMEContext 699 if (mBatchEditCount != 0) { 700 Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount); 701 mBatchEditCount = 0; 702 } 703 break; 704 705 case NOTIFY_IME_OF_BLUR: 706 break; 707 708 default: 709 if (DEBUG) { 710 throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type); 711 } 712 break; 713 } 714 } 715 716 @Override // SessionTextInput.EditableListener notifyIMEContext(final int state, final String typeHint, final String modeHint, final String actionHint, final int flags)717 public synchronized void notifyIMEContext(final int state, final String typeHint, 718 final String modeHint, final String actionHint, 719 final int flags) { 720 // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext, 721 // and not reset anywhere else. Usually, notifyIMEContext is called right after a 722 // focus or blur, so resetting mIMEState during the focus or blur seems harmless. 723 // However, this behavior is not guaranteed. Gecko may call notifyIMEContext 724 // independent of focus change; that is, a focus change may not be accompanied by 725 // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not 726 // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318) 727 /* When IME is 'disabled', IME processing is disabled. 728 In addition, the IME UI is hidden */ 729 mIMEState = state; 730 mIMEActionHint = (actionHint == null) ? "" : actionHint; 731 732 // These fields are reset here and will be updated when restartInput is called below 733 mUpdateRequest = null; 734 mCurrentInputMethod = ""; 735 } 736 } 737