summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java1060
1 files changed, 1060 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
new file mode 100644
index 000000000..a80be0bce
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -0,0 +1,1060 @@
+/* -*- 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.gecko;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.SynchronousQueue;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
+import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.annotation.SuppressLint;
+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.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Selection;
+import android.text.SpannableString;
+import android.text.method.KeyListener;
+import android.text.method.TextKeyListener;
+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.InputMethodManager;
+
+class GeckoInputConnection
+ extends BaseInputConnection
+ implements InputConnectionListener, GeckoEditableListener {
+
+ 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
+ private int mIMEState;
+ private String mIMETypeHint = "";
+ private String mIMEModeHint = "";
+ private String mIMEActionHint = "";
+ private boolean mFocused;
+
+ private String mCurrentInputMethod = "";
+
+ private final View mView;
+ private final GeckoEditableClient mEditableClient;
+ protected int mBatchEditCount;
+ private ExtractedTextRequest mUpdateRequest;
+ private final ExtractedText mUpdateExtract = new ExtractedText();
+ private final InputConnection mKeyInputConnection;
+ private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
+
+ // Prevent showSoftInput and hideSoftInput from causing reentrant calls on some devices.
+ private volatile boolean mSoftInputReentrancyGuard;
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ if (DEBUG)
+ return DebugGeckoInputConnection.create(targetView, editable);
+ else
+ return new GeckoInputConnection(targetView, editable);
+ }
+
+ protected GeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, true);
+ 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(int id) {
+ Editable editable = getEditable();
+ if (editable == null) {
+ return false;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ 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(editable);
+ editable.clear();
+ } else {
+ Clipboard.setText(
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd)));
+ editable.delete(selStart, selEnd);
+ }
+ break;
+ case android.R.id.paste:
+ commitText(Clipboard.getText(), 1);
+ break;
+ case android.R.id.copy:
+ // Copy the current selection or the empty string if nothing is selected.
+ String copiedText = selStart == selEnd ? "" :
+ editable.toString().substring(
+ Math.min(selStart, selEnd),
+ Math.max(selStart, selEnd));
+ Clipboard.setText(copiedText);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
+ if (req == null)
+ return null;
+
+ if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
+ mUpdateRequest = req;
+
+ Editable editable = getEditable();
+ if (editable == null) {
+ return null;
+ }
+ int selStart = Selection.getSelectionStart(editable);
+ int selEnd = Selection.getSelectionEnd(editable);
+
+ 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;
+ }
+
+ private View getView() {
+ return mView;
+ }
+
+ private InputMethodManager getInputMethodManager() {
+ View view = getView();
+ if (view == null) {
+ return null;
+ }
+ Context context = view.getContext();
+ return InputMethods.getInputMethodManager(context);
+ }
+
+ private void showSoftInput() {
+ if (mSoftInputReentrancyGuard) {
+ return;
+ }
+ final View v = getView();
+ final InputMethodManager imm = getInputMethodManager();
+ if (v == null || imm == null) {
+ return;
+ }
+
+ v.post(new Runnable() {
+ @Override
+ public void run() {
+ if (v.hasFocus() && !imm.isActive(v)) {
+ // Marshmallow workaround: The view has focus but it is not the active
+ // view for the input method. (Bug 1211848)
+ v.clearFocus();
+ v.requestFocus();
+ }
+ GeckoAppShell.getLayerView().getDynamicToolbarAnimator().showToolbar(/*immediately*/true);
+ mSoftInputReentrancyGuard = true;
+ imm.showSoftInput(v, 0);
+ mSoftInputReentrancyGuard = false;
+ }
+ });
+ }
+
+ private void hideSoftInput() {
+ if (mSoftInputReentrancyGuard) {
+ return;
+ }
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm != null) {
+ final View v = getView();
+ mSoftInputReentrancyGuard = true;
+ imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
+ mSoftInputReentrancyGuard = false;
+ }
+ }
+
+ private void restartInput() {
+
+ final InputMethodManager imm = getInputMethodManager();
+ if (imm == null) {
+ return;
+ }
+ final View v = getView();
+ // InputMethodManager has internal logic to detect if we are restarting input
+ // in an already focused View, which is the case here because all content text
+ // fields are inside one LayerView. When this happens, InputMethodManager will
+ // tell the input method to soft reset instead of hard reset. Stock latin IME
+ // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
+ // composition. The following workaround tricks the IME into clearing the
+ // composition when soft resetting.
+ if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) {
+ // Fake a selection change, because the IME clears the composition when
+ // the selection changes, even if soft-resetting. Offsets here must be
+ // different from the previous selection offsets, and -1 seems to be a
+ // reasonable, deterministic value
+ notifySelectionChange(-1, -1);
+ }
+ try {
+ imm.restartInput(v);
+ } catch (RuntimeException e) {
+ Log.e(LOGTAG, "Error restarting input", e);
+ }
+ }
+
+ private void resetInputConnection() {
+ if (mBatchEditCount != 0) {
+ Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
+ mBatchEditCount = 0;
+ }
+
+ // Do not reset mIMEState here; see comments in notifyIMEContext
+
+ restartInput();
+ }
+
+ @Override // GeckoEditableListener
+ public void onTextChange() {
+
+ if (mUpdateRequest == null) {
+ return;
+ }
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ mUpdateExtract.flags = 0;
+ // Update the entire Editable range
+ mUpdateExtract.partialStartOffset = -1;
+ mUpdateExtract.partialEndOffset = -1;
+ mUpdateExtract.selectionStart = Selection.getSelectionStart(editable);
+ mUpdateExtract.selectionEnd = Selection.getSelectionEnd(editable);
+ mUpdateExtract.startOffset = 0;
+ if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) {
+ mUpdateExtract.text = new SpannableString(editable);
+ } else {
+ mUpdateExtract.text = editable.toString();
+ }
+ imm.updateExtractedText(v, mUpdateRequest.token, mUpdateExtract);
+ }
+
+ @Override // GeckoEditableListener
+ public void onSelectionChange() {
+
+ final Editable editable = getEditable();
+ if (editable != null) {
+ notifySelectionChange(Selection.getSelectionStart(editable),
+ Selection.getSelectionEnd(editable));
+ }
+ }
+
+ private void notifySelectionChange(int start, int end) {
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ final Editable editable = getEditable();
+ if (imm == null || v == null || editable == null) {
+ return;
+ }
+ imm.updateSelection(v, start, end, getComposingSpanStart(editable),
+ getComposingSpanEnd(editable));
+ }
+
+ @Override
+ public void updateCompositionRects(final RectF[] aRects) {
+ if (!Versions.feature21Plus) {
+ return;
+ }
+
+ if (mCursorAnchorInfoBuilder == null) {
+ mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
+ }
+ mCursorAnchorInfoBuilder.reset();
+
+ // Calculate Gecko logical coords to screen coords
+ final View v = getView();
+ if (v == null) {
+ return;
+ }
+
+ int[] viewCoords = new int[2];
+ v.getLocationOnScreen(viewCoords);
+
+ DynamicToolbarAnimator animator = GeckoAppShell.getLayerView().getDynamicToolbarAnimator();
+ float toolbarHeight = animator.getMaxTranslation() - animator.getToolbarTranslation();
+
+ Matrix matrix = GeckoAppShell.getLayerView().getMatrixForLayerRectToViewRect();
+ if (matrix == null) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Cannot get Matrix to convert from Gecko coords to layer view coords");
+ }
+ return;
+ }
+ matrix.postTranslate(viewCoords[0], viewCoords[1] + toolbarHeight);
+ mCursorAnchorInfoBuilder.setMatrix(matrix);
+
+ final Editable content = getEditable();
+ if (content == null) {
+ return;
+ }
+ int composingStart = getComposingSpanStart(content);
+ int composingEnd = getComposingSpanEnd(content);
+ if (composingStart < 0 || composingEnd < 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "No composition for updates");
+ }
+ return;
+ }
+
+ for (int i = 0; i < aRects.length; i++) {
+ mCursorAnchorInfoBuilder.addCharacterBounds(i,
+ aRects[i].left,
+ aRects[i].top,
+ aRects[i].right,
+ aRects[i].bottom,
+ CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
+ }
+
+ mCursorAnchorInfoBuilder.setComposingText(0, content.subSequence(composingStart, composingEnd));
+
+ updateCursor();
+ }
+
+ @TargetApi(21)
+ private void updateCursor() {
+ if (mCursorAnchorInfoBuilder == null) {
+ return;
+ }
+
+ final InputMethodManager imm = getInputMethodManager();
+ final View v = getView();
+ if (imm == null || v == null) {
+ return;
+ }
+
+ imm.updateCursorAnchorInfo(v, mCursorAnchorInfoBuilder.build());
+ }
+
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.ONE_SHOT);
+ }
+
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0) {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.START_MONITOR);
+ } else {
+ mEditableClient.requestCursorUpdates(GeckoEditableClient.END_MONITOR);
+ }
+ return true;
+ }
+
+ @Override
+ public void onDefaultKeyEvent(final KeyEvent event) {
+ ThreadUtils.postToUiThread(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
+ 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 (InterruptedException e) {
+ }
+ }
+ return sBackgroundHandler;
+ }
+
+ private boolean canReturnCustomHandler() {
+ if (mIMEState == IME_STATE_DISABLED) {
+ return false;
+ }
+ for (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
+ return true;
+ }
+ 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;
+ }
+
+ // Android N: @Override // InputConnection
+ // We need to suppress lint complaining about the lack override here in the meantime: it wants us to build
+ // against sdk 24, even though we're using 23, and therefore complains about the lack of override.
+ // Once we update to 24, we can use the actual override annotation and remove the lint suppression.
+ @SuppressLint("Override")
+ public Handler getHandler() {
+ if (isPhysicalKeyboardPresent()) {
+ return ThreadUtils.getUiHandler();
+ }
+
+ return getBackgroundHandler();
+ }
+
+ @Override // InputConnectionListener
+ public Handler getHandler(Handler defHandler) {
+ if (!canReturnCustomHandler()) {
+ return defHandler;
+ }
+
+ return mEditableClient.setInputConnectionHandler(getHandler());
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ // Some keyboards require us to fill out outAttrs even if we return null.
+ outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
+ outAttrs.actionLabel = null;
+
+ if (mIMEState == IME_STATE_DISABLED) {
+ hideSoftInput();
+ return null;
+ }
+
+ if (mIMEState == IME_STATE_PASSWORD ||
+ "password".equalsIgnoreCase(mIMETypeHint))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
+ else if (mIMEState == IME_STATE_PLUGIN)
+ outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode
+ else if (mIMETypeHint.equalsIgnoreCase("url"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
+ else if (mIMETypeHint.equalsIgnoreCase("email"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ else if (mIMETypeHint.equalsIgnoreCase("tel"))
+ outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
+ else if (mIMETypeHint.equalsIgnoreCase("number") ||
+ mIMETypeHint.equalsIgnoreCase("range"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
+ | InputType.TYPE_NUMBER_FLAG_SIGNED
+ | InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMETypeHint.equalsIgnoreCase("week") ||
+ mIMETypeHint.equalsIgnoreCase("month"))
+ outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
+ | InputType.TYPE_DATETIME_VARIATION_DATE;
+ else if (mIMEModeHint.equalsIgnoreCase("numeric"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
+ InputType.TYPE_NUMBER_FLAG_SIGNED |
+ InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ else if (mIMEModeHint.equalsIgnoreCase("digit"))
+ outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
+ else {
+ // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+ InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
+ if (mIMETypeHint.equalsIgnoreCase("textarea") ||
+ mIMETypeHint.length() == 0) {
+ // empty mIMETypeHint indicates contentEditable/designMode documents
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ if (mIMEModeHint.equalsIgnoreCase("uppercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
+ else if (mIMETypeHint.equalsIgnoreCase("text") &&
+ !mIMEModeHint.equalsIgnoreCase("autocapitalized"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
+ else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
+ outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ // auto-capitalized mode is the default for types other than text
+ }
+
+ if (mIMEActionHint.equalsIgnoreCase("go"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
+ else if (mIMEActionHint.equalsIgnoreCase("done"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+ else if (mIMEActionHint.equalsIgnoreCase("next"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
+ else if (mIMEActionHint.equalsIgnoreCase("search") ||
+ mIMETypeHint.equalsIgnoreCase("search"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
+ else if (mIMEActionHint.equalsIgnoreCase("send"))
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
+ else if (mIMEActionHint.length() > 0) {
+ if (DEBUG)
+ Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
+ outAttrs.actionLabel = mIMEActionHint;
+ }
+
+ Context context = getView().getContext();
+ 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));
+ }
+
+ String prevInputMethod = mCurrentInputMethod;
+ mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
+ if (DEBUG) {
+ Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
+ }
+
+ if (mIMEState == IME_STATE_PLUGIN) {
+ // Since we are using a temporary string as the editable, the selection is at 0
+ outAttrs.initialSelStart = 0;
+ outAttrs.initialSelEnd = 0;
+ return mKeyInputConnection;
+ }
+ Editable editable = getEditable();
+ outAttrs.initialSelStart = Selection.getSelectionStart(editable);
+ outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
+
+ showSoftInput();
+ return this;
+ }
+
+ private boolean replaceComposingSpanWithSelection() {
+ final Editable content = getEditable();
+ if (content == null) {
+ return false;
+ }
+ int a = getComposingSpanStart(content),
+ 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(CharSequence text, 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(int start, 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);
+ }
+
+ /* package */ void sendKeyEvent(final int action, KeyEvent event) {
+ final Editable editable = getEditable();
+ if (editable == null) {
+ return;
+ }
+
+ final KeyListener keyListener = TextKeyListener.getInstance();
+ event = translateKey(event.getKeyCode(), event);
+
+ // We only let TextKeyListener do UI things on the UI thread.
+ final View v = ThreadUtils.isOnUiThread() ? getView() : null;
+ final int keyCode = event.getKeyCode();
+ final boolean handled;
+
+ if (shouldSkipKeyListener(keyCode, event)) {
+ handled = false;
+ } else if (action == KeyEvent.ACTION_DOWN) {
+ mEditableClient.setSuppressKeyUp(true);
+ handled = keyListener.onKeyDown(v, editable, keyCode, event);
+ } else if (action == KeyEvent.ACTION_UP) {
+ handled = keyListener.onKeyUp(v, editable, keyCode, event);
+ } else {
+ handled = keyListener.onKeyOther(v, editable, event);
+ }
+
+ if (!handled) {
+ mEditableClient.sendKeyEvent(event, action, TextKeyListener.getMetaState(editable));
+ }
+
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (!handled) {
+ // Usually, the down key listener call above adjusts meta states for us.
+ // However, if the call didn't handle the event, we have to manually
+ // adjust meta states so the meta states remain consistent.
+ TextKeyListener.adjustMetaAfterKeypress(editable);
+ }
+ mEditableClient.setSuppressKeyUp(false);
+ }
+ }
+
+ @Override
+ public boolean sendKeyEvent(KeyEvent event) {
+ sendKeyEvent(event.getAction(), event);
+ return false; // seems to always return false
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ private boolean shouldProcessKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_SEARCH:
+ // ignore HEADSETHOOK to allow hold-for-voice-search to work
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ return false;
+ }
+ return true;
+ }
+
+ private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) {
+ if (mIMEState == IME_STATE_DISABLED ||
+ mIMEState == IME_STATE_PLUGIN) {
+ return true;
+ }
+ // Preserve enter and tab keys for the browser
+ if (keyCode == KeyEvent.KEYCODE_ENTER ||
+ keyCode == KeyEvent.KEYCODE_TAB) {
+ return true;
+ }
+ // BaseKeyListener returns false even if it handled these keys for us,
+ // so we skip the key listener entirely and handle these ourselves
+ if (keyCode == KeyEvent.KEYCODE_DEL ||
+ keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ return true;
+ }
+ return false;
+ }
+
+ private KeyEvent translateKey(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
+ mIMEActionHint.equalsIgnoreCase("next")) {
+ return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
+ }
+ break;
+ }
+
+ if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) {
+ return GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event);
+ }
+
+ return event;
+ }
+
+ // Called by OnDefaultKeyEvent handler, up from Gecko
+ /* package */ void performDefaultKeyAction(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 (AppConstants.Versions.feature19Plus) {
+ // dispatchMediaKeyEvent is only available on Android 4.4+
+ Context viewContext = getView().getContext();
+ AudioManager am = (AudioManager)viewContext.getSystemService(Context.AUDIO_SERVICE);
+ am.dispatchMediaKeyEvent(event);
+ }
+ break;
+ }
+ }
+
+ private boolean processKey(final int action, final int keyCode, final KeyEvent event) {
+
+ if (keyCode > KeyEvent.getMaxKeyCode() || !shouldProcessKey(keyCode, event)) {
+ return false;
+ }
+
+ mEditableClient.postToInputConnection(new Runnable() {
+ @Override
+ public void run() {
+ sendKeyEvent(action, event);
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_DOWN, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return processKey(KeyEvent.ACTION_UP, keyCode, event);
+ }
+
+ /**
+ * Get a key that represents a given character.
+ */
+ private KeyEvent getCharKeyEvent(final char c) {
+ final long time = SystemClock.uptimeMillis();
+ return new KeyEvent(time, time, KeyEvent.ACTION_MULTIPLE,
+ KeyEvent.KEYCODE_UNKNOWN, /* repeat */ 0) {
+ @Override
+ public int getUnicodeChar() {
+ return c;
+ }
+
+ @Override
+ public int getUnicodeChar(int metaState) {
+ return c;
+ }
+ };
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
+ // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
+ final String str = event.getCharacters();
+ for (int i = 0; i < str.length(); i++) {
+ final KeyEvent charEvent = getCharKeyEvent(str.charAt(i));
+ if (!processKey(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN, charEvent) ||
+ !processKey(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN, charEvent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ while ((repeatCount--) != 0) {
+ if (!processKey(KeyEvent.ACTION_DOWN, keyCode, event) ||
+ !processKey(KeyEvent.ACTION_UP, keyCode, event)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ View v = getView();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ InputMethodManager imm = getInputMethodManager();
+ imm.toggleSoftInputFromWindow(v.getWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isIMEEnabled() {
+ // make sure this picks up PASSWORD and PLUGIN states as well
+ return mIMEState != IME_STATE_DISABLED;
+ }
+
+ @Override
+ public void notifyIME(int type) {
+ switch (type) {
+
+ case NOTIFY_IME_OF_FOCUS:
+ // Showing/hiding vkb is done in notifyIMEContext
+ mFocused = true;
+ resetInputConnection();
+ break;
+
+ case NOTIFY_IME_OF_BLUR:
+ // Showing/hiding vkb is done in notifyIMEContext
+ mFocused = false;
+ break;
+
+ case NOTIFY_IME_OPEN_VKB:
+ showSoftInput();
+ break;
+
+ default:
+ if (DEBUG) {
+ throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) {
+ // For some input type we will use a widget to display the ui, for those we must not
+ // display the ime. We can display a widget for date and time types and, if the sdk version
+ // is 11 or greater, for datetime/month/week as well.
+ if (typeHint != null &&
+ (typeHint.equalsIgnoreCase("date") ||
+ typeHint.equalsIgnoreCase("time") ||
+ typeHint.equalsIgnoreCase("datetime") ||
+ typeHint.equalsIgnoreCase("month") ||
+ typeHint.equalsIgnoreCase("week") ||
+ typeHint.equalsIgnoreCase("datetime-local"))) {
+ state = IME_STATE_DISABLED;
+ }
+
+ // 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;
+ mIMETypeHint = (typeHint == null) ? "" : typeHint;
+ mIMEModeHint = (modeHint == null) ? "" : modeHint;
+ mIMEActionHint = (actionHint == null) ? "" : actionHint;
+
+ // These fields are reset here and will be updated when restartInput is called below
+ mUpdateRequest = null;
+ mCurrentInputMethod = "";
+
+ View v = getView();
+ if (v == null || !v.hasFocus()) {
+ // When using Find In Page, we can still receive notifyIMEContext calls due to the
+ // selection changing when highlighting. However in this case we don't want to reset/
+ // show/hide the keyboard because the find box has the focus and is taking input from
+ // the keyboard.
+ return;
+ }
+
+ // On focus, the notifyIMEContext call comes *before* the
+ // notifyIME(NOTIFY_IME_OF_FOCUS) call, but we need to call restartInput during
+ // notifyIME, so we skip restartInput here. On blur, the notifyIMEContext call
+ // comes *after* the notifyIME(NOTIFY_IME_OF_BLUR) call, and we need to call
+ // restartInput here.
+ if (mIMEState == IME_STATE_DISABLED || mFocused) {
+ restartInput();
+ }
+ }
+}
+
+final class DebugGeckoInputConnection
+ extends GeckoInputConnection
+ implements InvocationHandler {
+
+ private InputConnection mProxy;
+ private final StringBuilder mCallLevel;
+
+ private DebugGeckoInputConnection(View targetView,
+ GeckoEditableClient editable) {
+ super(targetView, editable);
+ mCallLevel = new StringBuilder();
+ }
+
+ public static GeckoEditableListener create(View targetView,
+ GeckoEditableClient editable) {
+ final Class<?>[] PROXY_INTERFACES = { InputConnection.class,
+ InputConnectionListener.class,
+ GeckoEditableListener.class };
+ DebugGeckoInputConnection dgic =
+ new DebugGeckoInputConnection(targetView, editable);
+ dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
+ GeckoInputConnection.class.getClassLoader(),
+ PROXY_INTERFACES, dgic);
+ return (GeckoEditableListener)dgic.mProxy;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+
+ StringBuilder log = new StringBuilder(mCallLevel);
+ log.append("> ").append(method.getName()).append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ // translate argument values to constant names
+ if ("notifyIME".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.class, "NOTIFY_IME_", arg));
+ } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) {
+ log.append(GeckoEditable.getConstantName(
+ GeckoEditableListener.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(this, args);
+ if (ret == this) {
+ ret = mProxy;
+ }
+ 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;
+ }
+}