summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java829
1 files changed, 829 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java
new file mode 100644
index 0000000000..2d2f2d8dd3
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java
@@ -0,0 +1,829 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableString;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
+import androidx.annotation.NonNull;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import org.mozilla.gecko.Clipboard;
+import org.mozilla.gecko.InputMethods;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/* package */ final class GeckoInputConnection extends BaseInputConnection
+ implements SessionTextInput.InputConnectionClient, SessionTextInput.EditableListener {
+
+ private static final boolean DEBUG = false;
+ protected static final String LOGTAG = "GeckoInputConnection";
+
+ private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
+ private static final String CUSTOM_HANDLER_TEST_CLASS =
+ "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
+
+ private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
+
+ private static Handler sBackgroundHandler;
+
+ // Managed only by notifyIMEContext; see comments in notifyIMEContext
+ @IMEState private int mIMEState;
+ private String mIMEActionHint = "";
+ private int mLastSelectionStart;
+ private int mLastSelectionEnd;
+
+ private String mCurrentInputMethod = "";
+
+ private final GeckoSession mSession;
+ private final View mView;
+ private final SessionTextInput.EditableClient mEditableClient;
+ protected int mBatchEditCount;
+ private ExtractedTextRequest mUpdateRequest;
+ private final InputConnection mKeyInputConnection;
+ private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
+
+ public static SessionTextInput.InputConnectionClient create(
+ final GeckoSession session,
+ final View targetView,
+ final SessionTextInput.EditableClient editable) {
+ SessionTextInput.InputConnectionClient ic =
+ new GeckoInputConnection(session, targetView, editable);
+ if (DEBUG) {
+ ic = wrapForDebug(ic);
+ }
+ return ic;
+ }
+
+ private static SessionTextInput.InputConnectionClient wrapForDebug(
+ final SessionTextInput.InputConnectionClient ic) {
+ final InvocationHandler handler =
+ new InvocationHandler() {
+ private final StringBuilder mCallLevel = new StringBuilder();
+
+ @Override
+ public Object invoke(final Object proxy, final Method method, final Object[] args)
+ throws Throwable {
+ final StringBuilder log = new StringBuilder(mCallLevel);
+ log.append("> ").append(method.getName()).append("(");
+ if (args != null) {
+ for (int i = 0; i < args.length; i++) {
+ final Object arg = args[i];
+ // translate argument values to constant names
+ if ("notifyIME".equals(method.getName()) && i == 0) {
+ log.append(
+ GeckoEditable.getConstantName(
+ SessionTextInput.EditableListener.class, "NOTIFY_IME_", arg));
+ } else if ("notifyIMEContext".equals(method.getName()) && i == 0) {
+ log.append(
+ GeckoEditable.getConstantName(
+ SessionTextInput.EditableListener.class, "IME_STATE_", arg));
+ } else {
+ GeckoEditable.debugAppend(log, arg);
+ }
+ log.append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ log.append(")");
+ Log.d(LOGTAG, log.toString());
+
+ mCallLevel.append(' ');
+ Object ret = method.invoke(ic, args);
+ if (ret == ic) {
+ ret = proxy;
+ }
+ mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1));
+
+ log.setLength(mCallLevel.length());
+ log.append("< ").append(method.getName());
+ if (!method.getReturnType().equals(Void.TYPE)) {
+ GeckoEditable.debugAppend(log.append(": "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ return ret;
+ }
+ };
+
+ return (SessionTextInput.InputConnectionClient)
+ Proxy.newProxyInstance(
+ GeckoInputConnection.class.getClassLoader(),
+ new Class<?>[] {
+ InputConnection.class,
+ SessionTextInput.InputConnectionClient.class,
+ SessionTextInput.EditableListener.class
+ },
+ handler);
+ }
+
+ protected GeckoInputConnection(
+ final GeckoSession session,
+ final View targetView,
+ final SessionTextInput.EditableClient editable) {
+ super(targetView, true);
+ mSession = session;
+ mView = targetView;
+ mEditableClient = editable;
+ mIMEState = IME_STATE_DISABLED;
+ // InputConnection that sends keys for plugins, which don't have full editors
+ mKeyInputConnection = new BaseInputConnection(targetView, false);
+ }
+
+ @Override
+ public synchronized boolean beginBatchEdit() {
+ mBatchEditCount++;
+ if (mBatchEditCount == 1) {
+ mEditableClient.setBatchMode(true);
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized boolean endBatchEdit() {
+ if (mBatchEditCount <= 0) {
+ Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount <= 0?!");
+ return true;
+ }
+
+ mBatchEditCount--;
+ if (mBatchEditCount != 0) {
+ return true;
+ }
+
+ // setBatchMode will call onTextChange and/or onSelectionChange for us.
+ mEditableClient.setBatchMode(false);
+ return true;
+ }
+
+ @Override
+ public Editable getEditable() {
+ return mEditableClient.getEditable();
+ }
+
+ @Override
+ public boolean performContextMenuAction(final int id) {
+ final View view = getView();
+ final Editable editable = getEditable();
+ if (view == null || editable == null) {
+ return false;
+ }
+ final int selStart = Selection.getSelectionStart(editable);
+ final int selEnd = Selection.getSelectionEnd(editable);
+
+ switch (id) {
+ case android.R.id.selectAll:
+ setSelection(0, editable.length());
+ break;
+ case android.R.id.cut:
+ // If selection is empty, we'll select everything
+ if (selStart == selEnd) {
+ // Fill the clipboard
+ Clipboard.setText(view.getContext(), editable);
+ editable.clear();
+ } else {
+ Clipboard.setText(
+ view.getContext(),
+ editable.subSequence(Math.min(selStart, selEnd), Math.max(selStart, selEnd)));
+ editable.delete(selStart, selEnd);
+ }
+ break;
+ case android.R.id.paste:
+ final String text = Clipboard.getText(view.getContext());
+ if (text != null) {
+ commitText(text, 1);
+ }
+ break;
+ case android.R.id.copy:
+ // Copy the current selection or the empty string if nothing is selected.
+ final String copiedText =
+ selStart == selEnd
+ ? ""
+ : editable
+ .toString()
+ .substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd));
+ Clipboard.setText(view.getContext(), copiedText);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean performEditorAction(final int editorAction) {
+ if (editorAction == EditorInfo.IME_ACTION_PREVIOUS && !mIMEActionHint.equals("previous")) {
+ // This action is [Previous] key on FireTV's keyboard.
+ // [Previous] closes software keyboard, and don't generate any keyboard event.
+ getView()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ getInputDelegate().hideSoftInput(mSession);
+ }
+ });
+ return true;
+ }
+ return super.performEditorAction(editorAction);
+ }
+
+ @Override
+ public ExtractedText getExtractedText(final ExtractedTextRequest req, final int flags) {
+ if (req == null) return null;
+
+ if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) mUpdateRequest = req;
+
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return null;
+ }
+ final int selStart = Selection.getSelectionStart(editable);
+ final int selEnd = Selection.getSelectionEnd(editable);
+
+ final ExtractedText extract = new ExtractedText();
+ extract.flags = 0;
+ extract.partialStartOffset = -1;
+ extract.partialEndOffset = -1;
+ extract.selectionStart = selStart;
+ extract.selectionEnd = selEnd;
+ extract.startOffset = 0;
+ if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
+ extract.text = new SpannableString(editable);
+ } else {
+ extract.text = editable.toString();
+ }
+ return extract;
+ }
+
+ @Override // SessionTextInput.InputConnectionClient
+ public View getView() {
+ return mView;
+ }
+
+ @NonNull
+ /* package */ GeckoSession.TextInputDelegate getInputDelegate() {
+ return mSession.getTextInput().getDelegate();
+ }
+
+ @Override // SessionTextInput.EditableListener
+ public void onTextChange() {
+ final Editable editable = getEditable();
+ if (mUpdateRequest == null || editable == null) {
+ return;
+ }
+
+ final ExtractedTextRequest request = mUpdateRequest;
+ final ExtractedText extractedText = new ExtractedText();
+ extractedText.flags = 0;
+ // Update the entire Editable range
+ extractedText.partialStartOffset = -1;
+ extractedText.partialEndOffset = -1;
+ extractedText.selectionStart = Selection.getSelectionStart(editable);
+ extractedText.selectionEnd = Selection.getSelectionEnd(editable);
+ extractedText.startOffset = 0;
+ if ((request.flags & GET_TEXT_WITH_STYLES) != 0) {
+ extractedText.text = new SpannableString(editable);
+ } else {
+ extractedText.text = editable.toString();
+ }
+
+ getView()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ getInputDelegate().updateExtractedText(mSession, request, extractedText);
+ }
+ });
+ }
+
+ @Override // SessionTextInput.EditableListener
+ public void onSelectionChange() {
+
+ final Editable editable = getEditable();
+ if (editable != null) {
+ mLastSelectionStart = Selection.getSelectionStart(editable);
+ mLastSelectionEnd = Selection.getSelectionEnd(editable);
+ notifySelectionChange(mLastSelectionStart, mLastSelectionEnd);
+ }
+ }
+
+ private void notifySelectionChange(final int start, final int end) {
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return;
+ }
+
+ final int compositionStart = getComposingSpanStart(editable);
+ final int compositionEnd = getComposingSpanEnd(editable);
+
+ getView()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ getInputDelegate()
+ .updateSelection(mSession, start, end, compositionStart, compositionEnd);
+ }
+ });
+ }
+
+ @Override // SessionTextInput.EditableListener
+ public void onDiscardComposition() {
+ final View view = getView();
+ if (view == null) {
+ return;
+ }
+
+ // InputMethodManager.updateSelection will remove composition
+ // on most IMEs. But ATOK series do nothing. So we have to
+ // restart input method to remove composition as workaround.
+ if (!InputMethods.needsRestartInput(InputMethods.getCurrentInputMethod(view.getContext()))) {
+ return;
+ }
+
+ view.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ getInputDelegate()
+ .restartInput(
+ mSession, GeckoSession.TextInputDelegate.RESTART_REASON_CONTENT_CHANGE);
+ }
+ });
+ }
+
+ @TargetApi(21)
+ @Override // SessionTextInput.EditableListener
+ public void updateCompositionRects(final RectF[] rects, final RectF caretRect) {
+ if (!(Build.VERSION.SDK_INT >= 21)) {
+ return;
+ }
+
+ final View view = getView();
+ if (view == null) {
+ return;
+ }
+
+ final Editable content = getEditable();
+ if (content == null) {
+ return;
+ }
+
+ final int composingStart = getComposingSpanStart(content);
+ final int composingEnd = getComposingSpanEnd(content);
+ if (composingStart < 0 || composingEnd < 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "No composition for updates");
+ }
+ return;
+ }
+
+ final CharSequence composition = content.subSequence(composingStart, composingEnd);
+
+ view.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ updateCompositionRectsOnUi(view, rects, caretRect, composition);
+ }
+ });
+ }
+
+ @TargetApi(21)
+ /* package */ void updateCompositionRectsOnUi(
+ final View view, final RectF[] rects, final RectF caretRect, final CharSequence composition) {
+ if (mCursorAnchorInfoBuilder == null) {
+ mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
+ }
+ mCursorAnchorInfoBuilder.reset();
+
+ final Matrix matrix = new Matrix();
+ mSession.getClientToScreenOffsetMatrix(matrix);
+ mCursorAnchorInfoBuilder.setMatrix(matrix);
+
+ for (int i = 0; i < rects.length; i++) {
+ mCursorAnchorInfoBuilder.addCharacterBounds(
+ i,
+ rects[i].left,
+ rects[i].top,
+ rects[i].right,
+ rects[i].bottom,
+ CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
+ }
+
+ mCursorAnchorInfoBuilder.setComposingText(0, composition);
+
+ if (!caretRect.isEmpty()) {
+ // Gecko doesn't provide baseline information of caret.
+ mCursorAnchorInfoBuilder.setInsertionMarkerLocation(
+ caretRect.left,
+ caretRect.top,
+ caretRect.bottom,
+ caretRect.bottom,
+ CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
+ }
+
+ final CursorAnchorInfo info = mCursorAnchorInfoBuilder.build();
+ getView()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ getInputDelegate().updateCursorAnchorInfo(mSession, info);
+ }
+ });
+ }
+
+ @Override
+ public boolean requestCursorUpdates(final int cursorUpdateMode) {
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+ mEditableClient.requestCursorUpdates(SessionTextInput.EditableClient.ONE_SHOT);
+ }
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0) {
+ mEditableClient.requestCursorUpdates(SessionTextInput.EditableClient.START_MONITOR);
+ } else {
+ mEditableClient.requestCursorUpdates(SessionTextInput.EditableClient.END_MONITOR);
+ }
+ return true;
+ }
+
+ @Override // SessionTextInput.EditableListener
+ public void onDefaultKeyEvent(final KeyEvent event) {
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ GeckoInputConnection.this.performDefaultKeyAction(event);
+ }
+ });
+ }
+
+ private static synchronized Handler getBackgroundHandler() {
+ if (sBackgroundHandler != null) {
+ return sBackgroundHandler;
+ }
+ // Don't use GeckoBackgroundThread because Gecko thread may block waiting on
+ // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME,
+ // GeckoBackgroundThread may end up also block waiting on Gecko thread and a
+ // deadlock occurs
+ final Thread backgroundThread =
+ new Thread(
+ new Runnable() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (GeckoInputConnection.class) {
+ sBackgroundHandler = new Handler();
+ GeckoInputConnection.class.notify();
+ }
+ Looper.loop();
+ // We should never be exiting the thread loop.
+ throw new IllegalThreadStateException("unreachable code");
+ }
+ },
+ LOGTAG);
+ backgroundThread.setDaemon(true);
+ backgroundThread.start();
+ while (sBackgroundHandler == null) {
+ try {
+ // wait for new thread to set sBackgroundHandler
+ GeckoInputConnection.class.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ return sBackgroundHandler;
+ }
+
+ private synchronized boolean canReturnCustomHandler() {
+ if (mIMEState == IME_STATE_DISABLED) {
+ return false;
+ }
+ for (final StackTraceElement frame : Thread.currentThread().getStackTrace()) {
+ // We only return our custom Handler to InputMethodManager's InputConnection
+ // proxy. For all other purposes, we return the regular Handler.
+ // InputMethodManager retrieves the Handler for its InputConnection proxy
+ // inside its method startInputInner(), so we check for that here. This is
+ // valid from Android 2.2 to at least Android 4.2. If this situation ever
+ // changes, we gracefully fall back to using the regular Handler.
+ if ("startInputInner".equals(frame.getMethodName())
+ && "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
+ // Only return our own Handler to InputMethodManager and only prior to 24.
+ return Build.VERSION.SDK_INT < 24;
+ }
+ if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName())
+ && CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
+ // InputConnection tests should also run on the custom handler
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPhysicalKeyboardPresent() {
+ final View v = getView();
+ if (v == null) {
+ return false;
+ }
+ final Configuration config = v.getContext().getResources().getConfiguration();
+ return config.keyboard != Configuration.KEYBOARD_NOKEYS;
+ }
+
+ @Override // InputConnection
+ public Handler getHandler() {
+ final Handler handler;
+ if (isPhysicalKeyboardPresent()) {
+ handler = ThreadUtils.getUiHandler();
+ } else {
+ handler = getBackgroundHandler();
+ }
+ return mEditableClient.setInputConnectionHandler(handler);
+ }
+
+ @Override // SessionTextInput.InputConnectionClient
+ public Handler getHandler(final Handler defHandler) {
+ if (!canReturnCustomHandler()) {
+ return defHandler;
+ }
+
+ return getHandler();
+ }
+
+ @Override // InputConnection
+ public void closeConnection() {
+ if (mBatchEditCount != 0) {
+ // GBoard may call this into batch edit mode then it doesn't call endBatchEdit.
+ // Since we are recycle GeckoInputConnection, we have to reset
+ // batch count even if IME/keyboard bug.
+ if (DEBUG) {
+ Log.d(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
+ }
+ mBatchEditCount = 0;
+ // setBatchMode will call onTextChange and/or onSelectionChange for us.
+ mEditableClient.setBatchMode(false);
+ }
+ super.closeConnection();
+ }
+
+ @Override // SessionTextInput.InputConnectionClient
+ public synchronized InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
+ if (mIMEState == IME_STATE_DISABLED) {
+ return null;
+ }
+
+ final Context context = getView().getContext();
+ final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
+ // prevent showing full-screen keyboard only when the screen is tall enough
+ // to show some reasonable amount of the page (see bug 752709)
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN;
+ }
+
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "mapped IME states to: inputType = "
+ + Integer.toHexString(outAttrs.inputType)
+ + ", imeOptions = "
+ + Integer.toHexString(outAttrs.imeOptions));
+ }
+
+ final String prevInputMethod = mCurrentInputMethod;
+ mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
+ if (DEBUG) {
+ Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
+ }
+
+ outAttrs.initialSelStart = mLastSelectionStart;
+ outAttrs.initialSelEnd = mLastSelectionEnd;
+ return this;
+ }
+
+ private boolean replaceComposingSpanWithSelection() {
+ final Editable content = getEditable();
+ if (content == null) {
+ return false;
+ }
+ final int a = getComposingSpanStart(content);
+ final int b = getComposingSpanEnd(content);
+ if (a != -1 && b != -1) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "removing composition at " + a + "-" + b);
+ }
+ removeComposingSpans(content);
+ Selection.setSelection(content, a, b);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean commitText(final CharSequence text, final int newCursorPosition) {
+ if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod)
+ && text.length() == 1
+ && newCursorPosition > 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "committing \"" + text + "\" as key");
+ }
+ // mKeyInputConnection is a BaseInputConnection that commits text as keys;
+ // but we first need to replace any composing span with a selection,
+ // so that the new key events will generate characters to replace
+ // text from the old composing span
+ return replaceComposingSpanWithSelection()
+ && mKeyInputConnection.commitText(text, newCursorPosition);
+ }
+ return super.commitText(text, newCursorPosition);
+ }
+
+ @Override
+ public boolean setSelection(final int start, final int end) {
+ if (start < 0 || end < 0) {
+ // Some keyboards (e.g. Samsung) can call setSelection with
+ // negative offsets. In that case we ignore the call, similar to how
+ // BaseInputConnection.setSelection ignores offsets that go past the length.
+ return true;
+ }
+ return super.setSelection(start, end);
+ }
+
+ @Override
+ public boolean sendKeyEvent(final @NonNull KeyEvent event) {
+ final KeyEvent translatedEvent = translateKey(event.getKeyCode(), event);
+ mEditableClient.sendKeyEvent(getView(), event.getAction(), translatedEvent);
+ return false; // seems to always return false
+ }
+
+ private KeyEvent translateKey(final int keyCode, final @NonNull KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+ && mIMEActionHint.equals("maybenext")) {
+ // XXX It is not good to dispatch tab key for web compatibility.
+ // See https://github.com/w3c/uievents/issues/253 and bug 1600540.
+ return new KeyEvent(
+ event.getDownTime(),
+ event.getEventTime(),
+ event.getAction(),
+ KeyEvent.KEYCODE_TAB,
+ 0);
+ }
+ break;
+ }
+ return event;
+ }
+
+ // Called by OnDefaultKeyEvent handler, up from Gecko
+ /* package */ void performDefaultKeyAction(final KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MUTE:
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_REWIND:
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
+ case KeyEvent.KEYCODE_MEDIA_CLOSE:
+ case KeyEvent.KEYCODE_MEDIA_EJECT:
+ case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK:
+ // Forward media keypresses to the registered handler so headset controls work
+ // Does the same thing as Chromium
+ // https://chromium.googlesource.com/chromium/src/+/49.0.2623.67/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsDelegateAndroid.java#445
+ // These are all the keys dispatchMediaKeyEvent supports.
+ if (Build.VERSION.SDK_INT >= 19) {
+ // dispatchMediaKeyEvent is only available on Android 4.4+
+ final Context viewContext = getView().getContext();
+ final AudioManager am =
+ (AudioManager) viewContext.getSystemService(Context.AUDIO_SERVICE);
+ am.dispatchMediaKeyEvent(event);
+ }
+ break;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.N_MR1)
+ @Override
+ public boolean commitContent(
+ final InputContentInfo inputContentInfo, final int flags, final Bundle opts) {
+ final boolean requestPermission =
+ ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0);
+ if (requestPermission) {
+ try {
+ inputContentInfo.requestPermission();
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "InputContentInfo.requestPermission() failed.", e);
+ return false;
+ }
+ }
+
+ try (final InputStream inputStream =
+ getView()
+ .getContext()
+ .getContentResolver()
+ .openInputStream(inputContentInfo.getContentUri());
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ final byte[] data = new byte[4096];
+ int readed;
+ while ((readed = inputStream.read(data)) != -1) {
+ outputStream.write(data, 0, readed);
+ }
+ mEditableClient.insertImage(
+ outputStream.toByteArray(), inputContentInfo.getDescription().getMimeType(0));
+ } catch (final FileNotFoundException e) {
+ Log.e(LOGTAG, "Cannot open provider URI.", e);
+ return false;
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Cannot read/write provider URI.", e);
+ return false;
+ } finally {
+ if (requestPermission) {
+ inputContentInfo.releasePermission();
+ }
+ }
+
+ return true;
+ }
+
+ @Override // SessionTextInput.EditableListener
+ public void notifyIME(final @IMENotificationType int type) {
+ switch (type) {
+ case NOTIFY_IME_OF_FOCUS:
+ // Showing/hiding vkb is done in notifyIMEContext
+ if (mBatchEditCount != 0) {
+ Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
+ mBatchEditCount = 0;
+ }
+ break;
+
+ case NOTIFY_IME_OF_BLUR:
+ break;
+
+ case NOTIFY_IME_OF_TOKEN:
+ case NOTIFY_IME_OPEN_VKB:
+ case NOTIFY_IME_REPLY_EVENT:
+ case NOTIFY_IME_TO_CANCEL_COMPOSITION:
+ case NOTIFY_IME_TO_COMMIT_COMPOSITION:
+ default:
+ if (DEBUG) {
+ throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
+ }
+ break;
+ }
+ }
+
+ @Override // SessionTextInput.EditableListener
+ public synchronized void notifyIMEContext(
+ @IMEState final int state,
+ final String typeHint,
+ final String modeHint,
+ final String actionHint,
+ @IMEContextFlags final int flags) {
+ // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
+ // and not reset anywhere else. Usually, notifyIMEContext is called right after a
+ // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
+ // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
+ // independent of focus change; that is, a focus change may not be accompanied by
+ // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
+ // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
+ /* When IME is 'disabled', IME processing is disabled.
+ In addition, the IME UI is hidden */
+ mIMEState = state;
+ mIMEActionHint = (actionHint == null) ? "" : actionHint;
+
+ // These fields are reset here and will be updated when restartInput is called below
+ mUpdateRequest = null;
+ mCurrentInputMethod = "";
+ }
+}