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