/* -*- 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 org.mozilla.gecko.gfx.InputConnectionHandler; import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.ThreadUtils; import android.R; import android.content.Context; import android.os.Build; 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.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.concurrent.SynchronousQueue; class GeckoInputConnection extends BaseInputConnection implements InputConnectionHandler, GeckoEditableListener { private static final boolean DEBUG = false; protected static final String LOGTAG = "GeckoInputConnection"; private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480; private static Handler sBackgroundHandler; private static class InputThreadUtils { // We only want one UI editable around to keep synchronization simple, // so we make InputThreadUtils a singleton public static final InputThreadUtils sInstance = new InputThreadUtils(); private Editable mUiEditable; private Object mUiEditableReturn; private Exception mUiEditableException; private final SynchronousQueue mIcRunnableSync; private final Runnable mIcSignalRunnable; private InputThreadUtils() { mIcRunnableSync = new SynchronousQueue(); mIcSignalRunnable = new Runnable() { @Override public void run() { } }; } private void runOnIcThread(Handler icHandler, final Runnable runnable) { if (DEBUG) { ThreadUtils.assertOnUiThread(); Log.d(LOGTAG, "runOnIcThread() on thread " + icHandler.getLooper().getThread().getName()); } Runnable runner = new Runnable() { @Override public void run() { try { Runnable queuedRunnable = mIcRunnableSync.take(); if (DEBUG && queuedRunnable != runnable) { throw new IllegalThreadStateException("sync error"); } queuedRunnable.run(); } catch (InterruptedException e) { } } }; try { // if we are not inside waitForUiThread(), runner will call the runnable icHandler.post(runner); // runnable will be called by either runner from above or waitForUiThread() mIcRunnableSync.put(runnable); } catch (InterruptedException e) { } finally { // if waitForUiThread() already called runnable, runner should not call it again icHandler.removeCallbacks(runner); } } public void endWaitForUiThread() { if (DEBUG) { ThreadUtils.assertOnUiThread(); Log.d(LOGTAG, "endWaitForUiThread()"); } try { mIcRunnableSync.put(mIcSignalRunnable); } catch (InterruptedException e) { } } public void waitForUiThread(Handler icHandler) { if (DEBUG) { ThreadUtils.assertOnThread(icHandler.getLooper().getThread()); Log.d(LOGTAG, "waitForUiThread() blocking on thread " + icHandler.getLooper().getThread().getName()); } try { Runnable runnable = null; do { runnable = mIcRunnableSync.take(); runnable.run(); } while (runnable != mIcSignalRunnable); } catch (InterruptedException e) { } } public void runOnIcThread(final Handler uiHandler, final GeckoEditableClient client, final Runnable runnable) { final Handler icHandler = client.getInputConnectionHandler(); if (icHandler.getLooper() == uiHandler.getLooper()) { // IC thread is UI thread; safe to run directly runnable.run(); return; } runOnIcThread(icHandler, runnable); } public void sendEventFromUiThread(final Handler uiHandler, final GeckoEditableClient client, final GeckoEvent event) { runOnIcThread(uiHandler, client, new Runnable() { @Override public void run() { client.sendEvent(event); } }); } public Editable getEditableForUiThread(final Handler uiHandler, final GeckoEditableClient client) { if (DEBUG) { ThreadUtils.assertOnThread(uiHandler.getLooper().getThread()); } final Handler icHandler = client.getInputConnectionHandler(); if (icHandler.getLooper() == uiHandler.getLooper()) { // IC thread is UI thread; safe to use Editable directly return client.getEditable(); } // IC thread is not UI thread; we need to return a proxy Editable in order // to safely use the Editable from the UI thread if (mUiEditable != null) { return mUiEditable; } final InvocationHandler invokeEditable = new InvocationHandler() { @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { if (DEBUG) { ThreadUtils.assertOnThread(uiHandler.getLooper().getThread()); Log.d(LOGTAG, "UiEditable." + method.getName() + "() blocking"); } synchronized (icHandler) { // Now we are on UI thread mUiEditableReturn = null; mUiEditableException = null; // Post a Runnable that calls the real Editable and saves any // result/exception. Then wait on the Runnable to finish runOnIcThread(icHandler, new Runnable() { @Override public void run() { synchronized (icHandler) { try { mUiEditableReturn = method.invoke( client.getEditable(), args); } catch (Exception e) { mUiEditableException = e; } if (DEBUG) { Log.d(LOGTAG, "UiEditable." + method.getName() + "() returning"); } icHandler.notify(); } } }); // let InterruptedException propagate icHandler.wait(); if (mUiEditableException != null) { throw mUiEditableException; } return mUiEditableReturn; } } }; mUiEditable = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(), new Class[] { Editable.class }, invokeEditable); return mUiEditable; } } // Managed only by notifyIMEContext; see comments in notifyIMEContext private int mIMEState; private String mIMETypeHint = ""; private String mIMEModeHint = ""; private String mIMEActionHint = ""; private String mCurrentInputMethod = ""; private final GeckoEditableClient mEditableClient; protected int mBatchEditCount; private ExtractedTextRequest mUpdateRequest; private final ExtractedText mUpdateExtract = new ExtractedText(); private boolean mBatchSelectionChanged; private boolean mBatchTextChanged; private long mLastRestartInputTime; private final InputConnection mKeyInputConnection; 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); 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++; mEditableClient.setUpdateGecko(false); return true; } @Override public synchronized boolean endBatchEdit() { if (mBatchEditCount > 0) { mBatchEditCount--; if (mBatchEditCount == 0) { if (mBatchTextChanged) { notifyTextChange(); mBatchTextChanged = false; } if (mBatchSelectionChanged) { Editable editable = getEditable(); notifySelectionChange(Selection.getSelectionStart(editable), Selection.getSelectionEnd(editable)); mBatchSelectionChanged = false; } mEditableClient.setUpdateGecko(true); } } else { Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!"); } 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 R.id.selectAll: setSelection(0, editable.length()); break; case 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 R.id.paste: commitText(Clipboard.getText(), 1); break; case 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 static View getView() { return GeckoAppShell.getLayerView(); } private static InputMethodManager getInputMethodManager() { View view = getView(); if (view == null) { return null; } Context context = view.getContext(); return InputMethods.getInputMethodManager(context); } private static void showSoftInput() { final InputMethodManager imm = getInputMethodManager(); if (imm != null) { final View v = getView(); imm.showSoftInput(v, 0); } } private static void hideSoftInput() { final InputMethodManager imm = getInputMethodManager(); if (imm != null) { final View v = getView(); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } } private void tryRestartInput() { // Coalesce restartInput calls because InputMethodManager.restartInput() // is expensive and successive calls to it can lock up the keyboard if (SystemClock.uptimeMillis() < mLastRestartInputTime + 200) { return; } restartInput(); } private void restartInput() { mLastRestartInputTime = SystemClock.uptimeMillis(); 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); } imm.restartInput(v); } private void resetInputConnection() { if (mBatchEditCount != 0) { Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount); mBatchEditCount = 0; } mBatchSelectionChanged = false; mBatchTextChanged = false; // Do not reset mIMEState here; see comments in notifyIMEContext } @Override public void onTextChange(String text, int start, int oldEnd, int newEnd) { if (mUpdateRequest == null) { // Android always expects selection updates when not in extracted mode; // in extracted mode, the selection is reported through updateExtractedText final Editable editable = getEditable(); if (editable != null) { onSelectionChange(Selection.getSelectionStart(editable), Selection.getSelectionEnd(editable)); } return; } if (mBatchEditCount > 0) { // Delay notification until after the batch edit mBatchTextChanged = true; return; } notifyTextChange(); } private void notifyTextChange() { 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 public void onSelectionChange(int start, int end) { if (mBatchEditCount > 0) { // Delay notification until after the batch edit mBatchSelectionChanged = true; return; } notifySelectionChange(start, end); } 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)); } 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(); sBackgroundHandler = null; } }, 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; } } return false; } @Override public Handler getHandler(Handler defHandler) { if (!canReturnCustomHandler()) { return defHandler; } // getBackgroundHandler() is synchronized and requires locking, // but if we already have our handler, we don't have to lock final Handler newHandler = sBackgroundHandler != null ? sBackgroundHandler : getBackgroundHandler(); if (mEditableClient.setInputConnectionHandler(newHandler)) { return newHandler; } // Setting new IC handler failed; return old IC handler return mEditableClient.getInputConnectionHandler(); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (mIMEState == IME_STATE_DISABLED) { return null; } outAttrs.inputType = InputType.TYPE_CLASS_TEXT; outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; outAttrs.actionLabel = 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("search")) outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; 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 (mIMEModeHint.equalsIgnoreCase("autocapitalized")) outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; // lowercase mode is the default } 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")) 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 = GeckoAppShell.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 the user has changed IMEs, then notify input method observers. if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) { FormAssistPopup popup = GeckoAppShell.getGeckoInterface().getFormAssistPopup(); if (popup != null) { popup.onInputMethodChanged(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); 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); } @Override public boolean sendKeyEvent(KeyEvent event) { // BaseInputConnection.sendKeyEvent() dispatches the key event to the main thread. // In order to ensure events are processed in the proper order, we must block the // IC thread until the main thread finishes processing the key event super.sendKeyEvent(event); final View v = getView(); if (v == null) { return false; } final Handler icHandler = mEditableClient.getInputConnectionHandler(); final Handler mainHandler = v.getRootView().getHandler(); if (icHandler.getLooper() != mainHandler.getLooper()) { // We are on separate IC thread but the event is queued on the main thread; // wait on IC thread until the main thread processes our posted Runnable. At // that point the key event has already been processed. mainHandler.post(new Runnable() { @Override public void run() { InputThreadUtils.sInstance.endWaitForUiThread(); } }); InputThreadUtils.sInstance.waitForUiThread(icHandler); } 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: 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; } return event; } private boolean processKey(int keyCode, KeyEvent event, boolean down) { if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) { event = GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event); keyCode = event.getKeyCode(); } if (keyCode > KeyEvent.getMaxKeyCode() || !shouldProcessKey(keyCode, event)) { return false; } event = translateKey(keyCode, event); keyCode = event.getKeyCode(); View view = getView(); if (view == null) { InputThreadUtils.sInstance.sendEventFromUiThread(ThreadUtils.getUiHandler(), mEditableClient, GeckoEvent.createKeyEvent(event, 0)); return true; } // KeyListener returns true if it handled the event for us. KeyListener is only // safe to use on the UI thread; therefore we need to pass a proxy Editable to it KeyListener keyListener = TextKeyListener.getInstance(); Handler uiHandler = view.getRootView().getHandler(); Editable uiEditable = InputThreadUtils.sInstance. getEditableForUiThread(uiHandler, mEditableClient); boolean skip = shouldSkipKeyListener(keyCode, event); if (down) { mEditableClient.setSuppressKeyUp(true); } if (skip || (down && !keyListener.onKeyDown(view, uiEditable, keyCode, event)) || (!down && !keyListener.onKeyUp(view, uiEditable, keyCode, event))) { InputThreadUtils.sInstance.sendEventFromUiThread(uiHandler, mEditableClient, GeckoEvent.createKeyEvent(event, TextKeyListener.getMetaState(uiEditable))); if (skip && down) { // Usually, the down key listener call above adjusts meta states for us. // However, if we skip that call above, we have to manually adjust meta // states so the meta states remain consistent TextKeyListener.adjustMetaAfterKeypress(uiEditable); } } if (down) { mEditableClient.setSuppressKeyUp(false); } return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return processKey(keyCode, event, true); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return processKey(keyCode, event, false); } @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() View view = getView(); if (view != null) { InputThreadUtils.sInstance.runOnIcThread( view.getRootView().getHandler(), mEditableClient, new Runnable() { @Override public void run() { // Don't call GeckoInputConnection.commitText because it can // post a key event back to onKeyMultiple, causing a loop GeckoInputConnection.super.commitText(event.getCharacters(), 1); } }); } return true; } while ((repeatCount--) != 0) { if (!processKey(keyCode, event, true) || !processKey(keyCode, event, false)) { 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_TO_CANCEL_COMPOSITION: // Set composition to empty and end composition setComposingText("", 0); // Fall through case NOTIFY_IME_TO_COMMIT_COMPOSITION: // Commit and end composition finishComposingText(); tryRestartInput(); break; case NOTIFY_IME_OF_FOCUS: case NOTIFY_IME_OF_BLUR: // Showing/hiding vkb is done in notifyIMEContext resetInputConnection(); 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") || (Build.VERSION.SDK_INT >= 11 && (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; } restartInput(); if (mIMEState == IME_STATE_DISABLED) { hideSoftInput(); } else { showSoftInput(); } } } final class DebugGeckoInputConnection extends GeckoInputConnection implements InvocationHandler { private InputConnection mProxy; private 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, InputConnectionHandler.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("("); 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; } }