michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import java.lang.reflect.InvocationHandler; michael@0: import java.lang.reflect.Method; michael@0: import java.lang.reflect.Proxy; michael@0: import java.util.concurrent.SynchronousQueue; michael@0: michael@0: import org.mozilla.gecko.gfx.InputConnectionHandler; michael@0: import org.mozilla.gecko.util.Clipboard; michael@0: import org.mozilla.gecko.util.GamepadUtils; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; michael@0: michael@0: import android.R; michael@0: import android.content.Context; michael@0: import android.os.Build; michael@0: import android.os.Handler; michael@0: import android.os.Looper; michael@0: import android.os.SystemClock; michael@0: import android.text.Editable; michael@0: import android.text.InputType; michael@0: import android.text.Selection; michael@0: import android.text.SpannableString; michael@0: import android.text.method.KeyListener; michael@0: import android.text.method.TextKeyListener; michael@0: import android.util.DisplayMetrics; michael@0: import android.util.Log; michael@0: import android.view.KeyEvent; michael@0: import android.view.View; michael@0: import android.view.inputmethod.BaseInputConnection; michael@0: import android.view.inputmethod.EditorInfo; michael@0: import android.view.inputmethod.ExtractedText; michael@0: import android.view.inputmethod.ExtractedTextRequest; michael@0: import android.view.inputmethod.InputConnection; michael@0: import android.view.inputmethod.InputMethodManager; michael@0: michael@0: class GeckoInputConnection michael@0: extends BaseInputConnection michael@0: implements InputConnectionHandler, GeckoEditableListener { michael@0: michael@0: private static final boolean DEBUG = false; michael@0: protected static final String LOGTAG = "GeckoInputConnection"; michael@0: michael@0: private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection"; michael@0: private static final String CUSTOM_HANDLER_TEST_CLASS = michael@0: "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput"; michael@0: michael@0: private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480; michael@0: michael@0: private static Handler sBackgroundHandler; michael@0: michael@0: private static class InputThreadUtils { michael@0: // We only want one UI editable around to keep synchronization simple, michael@0: // so we make InputThreadUtils a singleton michael@0: public static final InputThreadUtils sInstance = new InputThreadUtils(); michael@0: michael@0: private Editable mUiEditable; michael@0: private Object mUiEditableReturn; michael@0: private Exception mUiEditableException; michael@0: private final SynchronousQueue mIcRunnableSync; michael@0: private final Runnable mIcSignalRunnable; michael@0: michael@0: private InputThreadUtils() { michael@0: mIcRunnableSync = new SynchronousQueue(); michael@0: mIcSignalRunnable = new Runnable() { michael@0: @Override public void run() { michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private void runOnIcThread(Handler icHandler, final Runnable runnable) { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: Log.d(LOGTAG, "runOnIcThread() on thread " + michael@0: icHandler.getLooper().getThread().getName()); michael@0: } michael@0: Runnable runner = new Runnable() { michael@0: @Override public void run() { michael@0: try { michael@0: Runnable queuedRunnable = mIcRunnableSync.take(); michael@0: if (DEBUG && queuedRunnable != runnable) { michael@0: throw new IllegalThreadStateException("sync error"); michael@0: } michael@0: queuedRunnable.run(); michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: }; michael@0: try { michael@0: // if we are not inside waitForUiThread(), runner will call the runnable michael@0: icHandler.post(runner); michael@0: // runnable will be called by either runner from above or waitForUiThread() michael@0: mIcRunnableSync.put(runnable); michael@0: } catch (InterruptedException e) { michael@0: } finally { michael@0: // if waitForUiThread() already called runnable, runner should not call it again michael@0: icHandler.removeCallbacks(runner); michael@0: } michael@0: } michael@0: michael@0: public void endWaitForUiThread() { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: Log.d(LOGTAG, "endWaitForUiThread()"); michael@0: } michael@0: try { michael@0: mIcRunnableSync.put(mIcSignalRunnable); michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: michael@0: public void waitForUiThread(Handler icHandler) { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnThread(icHandler.getLooper().getThread(), AssertBehavior.THROW); michael@0: Log.d(LOGTAG, "waitForUiThread() blocking on thread " + michael@0: icHandler.getLooper().getThread().getName()); michael@0: } michael@0: try { michael@0: Runnable runnable = null; michael@0: do { michael@0: runnable = mIcRunnableSync.take(); michael@0: runnable.run(); michael@0: } while (runnable != mIcSignalRunnable); michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: michael@0: public void runOnIcThread(final Handler uiHandler, michael@0: final GeckoEditableClient client, michael@0: final Runnable runnable) { michael@0: final Handler icHandler = client.getInputConnectionHandler(); michael@0: if (icHandler.getLooper() == uiHandler.getLooper()) { michael@0: // IC thread is UI thread; safe to run directly michael@0: runnable.run(); michael@0: return; michael@0: } michael@0: runOnIcThread(icHandler, runnable); michael@0: } michael@0: michael@0: public void sendEventFromUiThread(final Handler uiHandler, michael@0: final GeckoEditableClient client, michael@0: final GeckoEvent event) { michael@0: runOnIcThread(uiHandler, client, new Runnable() { michael@0: @Override public void run() { michael@0: client.sendEvent(event); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public Editable getEditableForUiThread(final Handler uiHandler, michael@0: final GeckoEditableClient client) { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnThread(uiHandler.getLooper().getThread(), AssertBehavior.THROW); michael@0: } michael@0: final Handler icHandler = client.getInputConnectionHandler(); michael@0: if (icHandler.getLooper() == uiHandler.getLooper()) { michael@0: // IC thread is UI thread; safe to use Editable directly michael@0: return client.getEditable(); michael@0: } michael@0: // IC thread is not UI thread; we need to return a proxy Editable in order michael@0: // to safely use the Editable from the UI thread michael@0: if (mUiEditable != null) { michael@0: return mUiEditable; michael@0: } michael@0: final InvocationHandler invokeEditable = new InvocationHandler() { michael@0: @Override public Object invoke(final Object proxy, michael@0: final Method method, michael@0: final Object[] args) throws Throwable { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnThread(uiHandler.getLooper().getThread(), AssertBehavior.THROW); michael@0: Log.d(LOGTAG, "UiEditable." + method.getName() + "() blocking"); michael@0: } michael@0: synchronized (icHandler) { michael@0: // Now we are on UI thread michael@0: mUiEditableReturn = null; michael@0: mUiEditableException = null; michael@0: // Post a Runnable that calls the real Editable and saves any michael@0: // result/exception. Then wait on the Runnable to finish michael@0: runOnIcThread(icHandler, new Runnable() { michael@0: @Override public void run() { michael@0: synchronized (icHandler) { michael@0: try { michael@0: mUiEditableReturn = method.invoke( michael@0: client.getEditable(), args); michael@0: } catch (Exception e) { michael@0: mUiEditableException = e; michael@0: } michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "UiEditable." + method.getName() + michael@0: "() returning"); michael@0: } michael@0: icHandler.notify(); michael@0: } michael@0: } michael@0: }); michael@0: // let InterruptedException propagate michael@0: icHandler.wait(); michael@0: if (mUiEditableException != null) { michael@0: throw mUiEditableException; michael@0: } michael@0: return mUiEditableReturn; michael@0: } michael@0: } michael@0: }; michael@0: mUiEditable = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(), michael@0: new Class[] { Editable.class }, invokeEditable); michael@0: return mUiEditable; michael@0: } michael@0: } michael@0: michael@0: // Managed only by notifyIMEContext; see comments in notifyIMEContext michael@0: private int mIMEState; michael@0: private String mIMETypeHint = ""; michael@0: private String mIMEModeHint = ""; michael@0: private String mIMEActionHint = ""; michael@0: michael@0: private String mCurrentInputMethod = ""; michael@0: michael@0: private final GeckoEditableClient mEditableClient; michael@0: protected int mBatchEditCount; michael@0: private ExtractedTextRequest mUpdateRequest; michael@0: private final ExtractedText mUpdateExtract = new ExtractedText(); michael@0: private boolean mBatchSelectionChanged; michael@0: private boolean mBatchTextChanged; michael@0: private long mLastRestartInputTime; michael@0: private final InputConnection mKeyInputConnection; michael@0: michael@0: public static GeckoEditableListener create(View targetView, michael@0: GeckoEditableClient editable) { michael@0: if (DEBUG) michael@0: return DebugGeckoInputConnection.create(targetView, editable); michael@0: else michael@0: return new GeckoInputConnection(targetView, editable); michael@0: } michael@0: michael@0: protected GeckoInputConnection(View targetView, michael@0: GeckoEditableClient editable) { michael@0: super(targetView, true); michael@0: mEditableClient = editable; michael@0: mIMEState = IME_STATE_DISABLED; michael@0: // InputConnection that sends keys for plugins, which don't have full editors michael@0: mKeyInputConnection = new BaseInputConnection(targetView, false); michael@0: } michael@0: michael@0: @Override michael@0: public synchronized boolean beginBatchEdit() { michael@0: mBatchEditCount++; michael@0: mEditableClient.setUpdateGecko(false); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public synchronized boolean endBatchEdit() { michael@0: if (mBatchEditCount > 0) { michael@0: mBatchEditCount--; michael@0: if (mBatchEditCount == 0) { michael@0: if (mBatchTextChanged) { michael@0: notifyTextChange(); michael@0: mBatchTextChanged = false; michael@0: } michael@0: if (mBatchSelectionChanged) { michael@0: Editable editable = getEditable(); michael@0: notifySelectionChange(Selection.getSelectionStart(editable), michael@0: Selection.getSelectionEnd(editable)); michael@0: mBatchSelectionChanged = false; michael@0: } michael@0: mEditableClient.setUpdateGecko(true); michael@0: } michael@0: } else { michael@0: Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!"); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public Editable getEditable() { michael@0: return mEditableClient.getEditable(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean performContextMenuAction(int id) { michael@0: Editable editable = getEditable(); michael@0: if (editable == null) { michael@0: return false; michael@0: } michael@0: int selStart = Selection.getSelectionStart(editable); michael@0: int selEnd = Selection.getSelectionEnd(editable); michael@0: michael@0: switch (id) { michael@0: case R.id.selectAll: michael@0: setSelection(0, editable.length()); michael@0: break; michael@0: case R.id.cut: michael@0: // If selection is empty, we'll select everything michael@0: if (selStart == selEnd) { michael@0: // Fill the clipboard michael@0: Clipboard.setText(editable); michael@0: editable.clear(); michael@0: } else { michael@0: Clipboard.setText( michael@0: editable.toString().substring( michael@0: Math.min(selStart, selEnd), michael@0: Math.max(selStart, selEnd))); michael@0: editable.delete(selStart, selEnd); michael@0: } michael@0: break; michael@0: case R.id.paste: michael@0: commitText(Clipboard.getText(), 1); michael@0: break; michael@0: case R.id.copy: michael@0: // Copy the current selection or the empty string if nothing is selected. michael@0: String copiedText = selStart == selEnd ? "" : michael@0: editable.toString().substring( michael@0: Math.min(selStart, selEnd), michael@0: Math.max(selStart, selEnd)); michael@0: Clipboard.setText(copiedText); michael@0: break; michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) { michael@0: if (req == null) michael@0: return null; michael@0: michael@0: if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) michael@0: mUpdateRequest = req; michael@0: michael@0: Editable editable = getEditable(); michael@0: if (editable == null) { michael@0: return null; michael@0: } michael@0: int selStart = Selection.getSelectionStart(editable); michael@0: int selEnd = Selection.getSelectionEnd(editable); michael@0: michael@0: ExtractedText extract = new ExtractedText(); michael@0: extract.flags = 0; michael@0: extract.partialStartOffset = -1; michael@0: extract.partialEndOffset = -1; michael@0: extract.selectionStart = selStart; michael@0: extract.selectionEnd = selEnd; michael@0: extract.startOffset = 0; michael@0: if ((req.flags & GET_TEXT_WITH_STYLES) != 0) { michael@0: extract.text = new SpannableString(editable); michael@0: } else { michael@0: extract.text = editable.toString(); michael@0: } michael@0: return extract; michael@0: } michael@0: michael@0: private static View getView() { michael@0: return GeckoAppShell.getLayerView(); michael@0: } michael@0: michael@0: private static InputMethodManager getInputMethodManager() { michael@0: View view = getView(); michael@0: if (view == null) { michael@0: return null; michael@0: } michael@0: Context context = view.getContext(); michael@0: return InputMethods.getInputMethodManager(context); michael@0: } michael@0: michael@0: private static void showSoftInput() { michael@0: final InputMethodManager imm = getInputMethodManager(); michael@0: if (imm != null) { michael@0: final View v = getView(); michael@0: imm.showSoftInput(v, 0); michael@0: } michael@0: } michael@0: michael@0: private static void hideSoftInput() { michael@0: final InputMethodManager imm = getInputMethodManager(); michael@0: if (imm != null) { michael@0: final View v = getView(); michael@0: imm.hideSoftInputFromWindow(v.getWindowToken(), 0); michael@0: } michael@0: } michael@0: michael@0: private void tryRestartInput() { michael@0: // Coalesce restartInput calls because InputMethodManager.restartInput() michael@0: // is expensive and successive calls to it can lock up the keyboard michael@0: if (SystemClock.uptimeMillis() < mLastRestartInputTime + 200) { michael@0: return; michael@0: } michael@0: restartInput(); michael@0: } michael@0: michael@0: private void restartInput() { michael@0: michael@0: mLastRestartInputTime = SystemClock.uptimeMillis(); michael@0: michael@0: final InputMethodManager imm = getInputMethodManager(); michael@0: if (imm == null) { michael@0: return; michael@0: } michael@0: final View v = getView(); michael@0: // InputMethodManager has internal logic to detect if we are restarting input michael@0: // in an already focused View, which is the case here because all content text michael@0: // fields are inside one LayerView. When this happens, InputMethodManager will michael@0: // tell the input method to soft reset instead of hard reset. Stock latin IME michael@0: // on Android 4.2+ has a quirk that when it soft resets, it does not clear the michael@0: // composition. The following workaround tricks the IME into clearing the michael@0: // composition when soft resetting. michael@0: if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) { michael@0: // Fake a selection change, because the IME clears the composition when michael@0: // the selection changes, even if soft-resetting. Offsets here must be michael@0: // different from the previous selection offsets, and -1 seems to be a michael@0: // reasonable, deterministic value michael@0: notifySelectionChange(-1, -1); michael@0: } michael@0: imm.restartInput(v); michael@0: } michael@0: michael@0: private void resetInputConnection() { michael@0: if (mBatchEditCount != 0) { michael@0: Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount); michael@0: mBatchEditCount = 0; michael@0: } michael@0: mBatchSelectionChanged = false; michael@0: mBatchTextChanged = false; michael@0: michael@0: // Do not reset mIMEState here; see comments in notifyIMEContext michael@0: } michael@0: michael@0: @Override michael@0: public void onTextChange(String text, int start, int oldEnd, int newEnd) { michael@0: michael@0: if (mUpdateRequest == null) { michael@0: // Android always expects selection updates when not in extracted mode; michael@0: // in extracted mode, the selection is reported through updateExtractedText michael@0: final Editable editable = getEditable(); michael@0: if (editable != null) { michael@0: onSelectionChange(Selection.getSelectionStart(editable), michael@0: Selection.getSelectionEnd(editable)); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (mBatchEditCount > 0) { michael@0: // Delay notification until after the batch edit michael@0: mBatchTextChanged = true; michael@0: return; michael@0: } michael@0: notifyTextChange(); michael@0: } michael@0: michael@0: private void notifyTextChange() { michael@0: michael@0: final InputMethodManager imm = getInputMethodManager(); michael@0: final View v = getView(); michael@0: final Editable editable = getEditable(); michael@0: if (imm == null || v == null || editable == null) { michael@0: return; michael@0: } michael@0: mUpdateExtract.flags = 0; michael@0: // Update the entire Editable range michael@0: mUpdateExtract.partialStartOffset = -1; michael@0: mUpdateExtract.partialEndOffset = -1; michael@0: mUpdateExtract.selectionStart = michael@0: Selection.getSelectionStart(editable); michael@0: mUpdateExtract.selectionEnd = michael@0: Selection.getSelectionEnd(editable); michael@0: mUpdateExtract.startOffset = 0; michael@0: if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) { michael@0: mUpdateExtract.text = new SpannableString(editable); michael@0: } else { michael@0: mUpdateExtract.text = editable.toString(); michael@0: } michael@0: imm.updateExtractedText(v, mUpdateRequest.token, michael@0: mUpdateExtract); michael@0: } michael@0: michael@0: @Override michael@0: public void onSelectionChange(int start, int end) { michael@0: michael@0: if (mBatchEditCount > 0) { michael@0: // Delay notification until after the batch edit michael@0: mBatchSelectionChanged = true; michael@0: return; michael@0: } michael@0: notifySelectionChange(start, end); michael@0: } michael@0: michael@0: private void notifySelectionChange(int start, int end) { michael@0: michael@0: final InputMethodManager imm = getInputMethodManager(); michael@0: final View v = getView(); michael@0: final Editable editable = getEditable(); michael@0: if (imm == null || v == null || editable == null) { michael@0: return; michael@0: } michael@0: imm.updateSelection(v, start, end, getComposingSpanStart(editable), michael@0: getComposingSpanEnd(editable)); michael@0: } michael@0: michael@0: private static synchronized Handler getBackgroundHandler() { michael@0: if (sBackgroundHandler != null) { michael@0: return sBackgroundHandler; michael@0: } michael@0: // Don't use GeckoBackgroundThread because Gecko thread may block waiting on michael@0: // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME, michael@0: // GeckoBackgroundThread may end up also block waiting on Gecko thread and a michael@0: // deadlock occurs michael@0: Thread backgroundThread = new Thread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Looper.prepare(); michael@0: synchronized (GeckoInputConnection.class) { michael@0: sBackgroundHandler = new Handler(); michael@0: GeckoInputConnection.class.notify(); michael@0: } michael@0: Looper.loop(); michael@0: sBackgroundHandler = null; michael@0: } michael@0: }, LOGTAG); michael@0: backgroundThread.setDaemon(true); michael@0: backgroundThread.start(); michael@0: while (sBackgroundHandler == null) { michael@0: try { michael@0: // wait for new thread to set sBackgroundHandler michael@0: GeckoInputConnection.class.wait(); michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: return sBackgroundHandler; michael@0: } michael@0: michael@0: private boolean canReturnCustomHandler() { michael@0: if (mIMEState == IME_STATE_DISABLED) { michael@0: return false; michael@0: } michael@0: for (StackTraceElement frame : Thread.currentThread().getStackTrace()) { michael@0: // We only return our custom Handler to InputMethodManager's InputConnection michael@0: // proxy. For all other purposes, we return the regular Handler. michael@0: // InputMethodManager retrieves the Handler for its InputConnection proxy michael@0: // inside its method startInputInner(), so we check for that here. This is michael@0: // valid from Android 2.2 to at least Android 4.2. If this situation ever michael@0: // changes, we gracefully fall back to using the regular Handler. michael@0: if ("startInputInner".equals(frame.getMethodName()) && michael@0: "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) { michael@0: // only return our own Handler to InputMethodManager michael@0: return true; michael@0: } michael@0: if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) && michael@0: CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) { michael@0: // InputConnection tests should also run on the custom handler michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public Handler getHandler(Handler defHandler) { michael@0: if (!canReturnCustomHandler()) { michael@0: return defHandler; michael@0: } michael@0: // getBackgroundHandler() is synchronized and requires locking, michael@0: // but if we already have our handler, we don't have to lock michael@0: final Handler newHandler = sBackgroundHandler != null michael@0: ? sBackgroundHandler michael@0: : getBackgroundHandler(); michael@0: if (mEditableClient.setInputConnectionHandler(newHandler)) { michael@0: return newHandler; michael@0: } michael@0: // Setting new IC handler failed; return old IC handler michael@0: return mEditableClient.getInputConnectionHandler(); michael@0: } michael@0: michael@0: @Override michael@0: public InputConnection onCreateInputConnection(EditorInfo outAttrs) { michael@0: if (mIMEState == IME_STATE_DISABLED) { michael@0: return null; michael@0: } michael@0: michael@0: outAttrs.inputType = InputType.TYPE_CLASS_TEXT; michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; michael@0: outAttrs.actionLabel = null; michael@0: michael@0: if (mIMEState == IME_STATE_PASSWORD || michael@0: "password".equalsIgnoreCase(mIMETypeHint)) michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; michael@0: else if (mIMEState == IME_STATE_PLUGIN) michael@0: outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode michael@0: else if (mIMETypeHint.equalsIgnoreCase("url")) michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI; michael@0: else if (mIMETypeHint.equalsIgnoreCase("email")) michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; michael@0: else if (mIMETypeHint.equalsIgnoreCase("search")) michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; michael@0: else if (mIMETypeHint.equalsIgnoreCase("tel")) michael@0: outAttrs.inputType = InputType.TYPE_CLASS_PHONE; michael@0: else if (mIMETypeHint.equalsIgnoreCase("number") || michael@0: mIMETypeHint.equalsIgnoreCase("range")) michael@0: outAttrs.inputType = InputType.TYPE_CLASS_NUMBER michael@0: | InputType.TYPE_NUMBER_FLAG_SIGNED michael@0: | InputType.TYPE_NUMBER_FLAG_DECIMAL; michael@0: else if (mIMETypeHint.equalsIgnoreCase("week") || michael@0: mIMETypeHint.equalsIgnoreCase("month")) michael@0: outAttrs.inputType = InputType.TYPE_CLASS_DATETIME michael@0: | InputType.TYPE_DATETIME_VARIATION_DATE; michael@0: else if (mIMEModeHint.equalsIgnoreCase("numeric")) michael@0: outAttrs.inputType = InputType.TYPE_CLASS_NUMBER | michael@0: InputType.TYPE_NUMBER_FLAG_SIGNED | michael@0: InputType.TYPE_NUMBER_FLAG_DECIMAL; michael@0: else if (mIMEModeHint.equalsIgnoreCase("digit")) michael@0: outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; michael@0: else { michael@0: // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | michael@0: InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE; michael@0: if (mIMETypeHint.equalsIgnoreCase("textarea") || michael@0: mIMETypeHint.length() == 0) { michael@0: // empty mIMETypeHint indicates contentEditable/designMode documents michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; michael@0: } michael@0: if (mIMEModeHint.equalsIgnoreCase("uppercase")) michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; michael@0: else if (mIMEModeHint.equalsIgnoreCase("titlecase")) michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; michael@0: else if (!mIMEModeHint.equalsIgnoreCase("lowercase")) michael@0: outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; michael@0: // auto-capitalized mode is the default michael@0: } michael@0: michael@0: if (mIMEActionHint.equalsIgnoreCase("go")) michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_GO; michael@0: else if (mIMEActionHint.equalsIgnoreCase("done")) michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; michael@0: else if (mIMEActionHint.equalsIgnoreCase("next")) michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; michael@0: else if (mIMEActionHint.equalsIgnoreCase("search")) michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; michael@0: else if (mIMEActionHint.equalsIgnoreCase("send")) michael@0: outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND; michael@0: else if (mIMEActionHint.length() > 0) { michael@0: if (DEBUG) michael@0: Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\""); michael@0: outAttrs.actionLabel = mIMEActionHint; michael@0: } michael@0: michael@0: Context context = GeckoAppShell.getContext(); michael@0: DisplayMetrics metrics = context.getResources().getDisplayMetrics(); michael@0: if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) { michael@0: // prevent showing full-screen keyboard only when the screen is tall enough michael@0: // to show some reasonable amount of the page (see bug 752709) michael@0: outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI michael@0: | EditorInfo.IME_FLAG_NO_FULLSCREEN; michael@0: } michael@0: michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "mapped IME states to: inputType = " + michael@0: Integer.toHexString(outAttrs.inputType) + ", imeOptions = " + michael@0: Integer.toHexString(outAttrs.imeOptions)); michael@0: } michael@0: michael@0: String prevInputMethod = mCurrentInputMethod; michael@0: mCurrentInputMethod = InputMethods.getCurrentInputMethod(context); michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod); michael@0: } michael@0: michael@0: // If the user has changed IMEs, then notify input method observers. michael@0: if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) { michael@0: FormAssistPopup popup = GeckoAppShell.getGeckoInterface().getFormAssistPopup(); michael@0: if (popup != null) { michael@0: popup.onInputMethodChanged(mCurrentInputMethod); michael@0: } michael@0: } michael@0: michael@0: if (mIMEState == IME_STATE_PLUGIN) { michael@0: // Since we are using a temporary string as the editable, the selection is at 0 michael@0: outAttrs.initialSelStart = 0; michael@0: outAttrs.initialSelEnd = 0; michael@0: return mKeyInputConnection; michael@0: } michael@0: Editable editable = getEditable(); michael@0: outAttrs.initialSelStart = Selection.getSelectionStart(editable); michael@0: outAttrs.initialSelEnd = Selection.getSelectionEnd(editable); michael@0: return this; michael@0: } michael@0: michael@0: private boolean replaceComposingSpanWithSelection() { michael@0: final Editable content = getEditable(); michael@0: if (content == null) { michael@0: return false; michael@0: } michael@0: int a = getComposingSpanStart(content), michael@0: b = getComposingSpanEnd(content); michael@0: if (a != -1 && b != -1) { michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "removing composition at " + a + "-" + b); michael@0: } michael@0: removeComposingSpans(content); michael@0: Selection.setSelection(content, a, b); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean commitText(CharSequence text, int newCursorPosition) { michael@0: if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) && michael@0: text.length() == 1 && newCursorPosition > 0) { michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "committing \"" + text + "\" as key"); michael@0: } michael@0: // mKeyInputConnection is a BaseInputConnection that commits text as keys; michael@0: // but we first need to replace any composing span with a selection, michael@0: // so that the new key events will generate characters to replace michael@0: // text from the old composing span michael@0: return replaceComposingSpanWithSelection() && michael@0: mKeyInputConnection.commitText(text, newCursorPosition); michael@0: } michael@0: return super.commitText(text, newCursorPosition); michael@0: } michael@0: michael@0: @Override michael@0: public boolean setSelection(int start, int end) { michael@0: if (start < 0 || end < 0) { michael@0: // Some keyboards (e.g. Samsung) can call setSelection with michael@0: // negative offsets. In that case we ignore the call, similar to how michael@0: // BaseInputConnection.setSelection ignores offsets that go past the length. michael@0: return true; michael@0: } michael@0: return super.setSelection(start, end); michael@0: } michael@0: michael@0: @Override michael@0: public boolean sendKeyEvent(KeyEvent event) { michael@0: // BaseInputConnection.sendKeyEvent() dispatches the key event to the main thread. michael@0: // In order to ensure events are processed in the proper order, we must block the michael@0: // IC thread until the main thread finishes processing the key event michael@0: super.sendKeyEvent(event); michael@0: final View v = getView(); michael@0: if (v == null) { michael@0: return false; michael@0: } michael@0: final Handler icHandler = mEditableClient.getInputConnectionHandler(); michael@0: final Handler mainHandler = v.getRootView().getHandler(); michael@0: if (icHandler.getLooper() != mainHandler.getLooper()) { michael@0: // We are on separate IC thread but the event is queued on the main thread; michael@0: // wait on IC thread until the main thread processes our posted Runnable. At michael@0: // that point the key event has already been processed. michael@0: mainHandler.post(new Runnable() { michael@0: @Override public void run() { michael@0: InputThreadUtils.sInstance.endWaitForUiThread(); michael@0: } michael@0: }); michael@0: InputThreadUtils.sInstance.waitForUiThread(icHandler); michael@0: } michael@0: return false; // seems to always return false michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyPreIme(int keyCode, KeyEvent event) { michael@0: return false; michael@0: } michael@0: michael@0: private boolean shouldProcessKey(int keyCode, KeyEvent event) { michael@0: switch (keyCode) { michael@0: case KeyEvent.KEYCODE_MENU: michael@0: case KeyEvent.KEYCODE_BACK: michael@0: case KeyEvent.KEYCODE_VOLUME_UP: michael@0: case KeyEvent.KEYCODE_VOLUME_DOWN: michael@0: case KeyEvent.KEYCODE_SEARCH: michael@0: return false; michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) { michael@0: if (mIMEState == IME_STATE_DISABLED || michael@0: mIMEState == IME_STATE_PLUGIN) { michael@0: return true; michael@0: } michael@0: // Preserve enter and tab keys for the browser michael@0: if (keyCode == KeyEvent.KEYCODE_ENTER || michael@0: keyCode == KeyEvent.KEYCODE_TAB) { michael@0: return true; michael@0: } michael@0: // BaseKeyListener returns false even if it handled these keys for us, michael@0: // so we skip the key listener entirely and handle these ourselves michael@0: if (keyCode == KeyEvent.KEYCODE_DEL || michael@0: keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: private KeyEvent translateKey(int keyCode, KeyEvent event) { michael@0: switch (keyCode) { michael@0: case KeyEvent.KEYCODE_ENTER: michael@0: if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 && michael@0: mIMEActionHint.equalsIgnoreCase("next")) { michael@0: return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB); michael@0: } michael@0: break; michael@0: } michael@0: return event; michael@0: } michael@0: michael@0: private boolean processKey(int keyCode, KeyEvent event, boolean down) { michael@0: if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) { michael@0: event = GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event); michael@0: keyCode = event.getKeyCode(); michael@0: } michael@0: michael@0: if (keyCode > KeyEvent.getMaxKeyCode() || michael@0: !shouldProcessKey(keyCode, event)) { michael@0: return false; michael@0: } michael@0: event = translateKey(keyCode, event); michael@0: keyCode = event.getKeyCode(); michael@0: michael@0: View view = getView(); michael@0: if (view == null) { michael@0: InputThreadUtils.sInstance.sendEventFromUiThread(ThreadUtils.getUiHandler(), michael@0: mEditableClient, GeckoEvent.createKeyEvent(event, 0)); michael@0: return true; michael@0: } michael@0: michael@0: // KeyListener returns true if it handled the event for us. KeyListener is only michael@0: // safe to use on the UI thread; therefore we need to pass a proxy Editable to it michael@0: KeyListener keyListener = TextKeyListener.getInstance(); michael@0: Handler uiHandler = view.getRootView().getHandler(); michael@0: Editable uiEditable = InputThreadUtils.sInstance. michael@0: getEditableForUiThread(uiHandler, mEditableClient); michael@0: boolean skip = shouldSkipKeyListener(keyCode, event); michael@0: if (down) { michael@0: mEditableClient.setSuppressKeyUp(true); michael@0: } michael@0: if (skip || michael@0: (down && !keyListener.onKeyDown(view, uiEditable, keyCode, event)) || michael@0: (!down && !keyListener.onKeyUp(view, uiEditable, keyCode, event))) { michael@0: InputThreadUtils.sInstance.sendEventFromUiThread(uiHandler, mEditableClient, michael@0: GeckoEvent.createKeyEvent(event, TextKeyListener.getMetaState(uiEditable))); michael@0: if (skip && down) { michael@0: // Usually, the down key listener call above adjusts meta states for us. michael@0: // However, if we skip that call above, we have to manually adjust meta michael@0: // states so the meta states remain consistent michael@0: TextKeyListener.adjustMetaAfterKeypress(uiEditable); michael@0: } michael@0: } michael@0: if (down) { michael@0: mEditableClient.setSuppressKeyUp(false); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyDown(int keyCode, KeyEvent event) { michael@0: return processKey(keyCode, event, true); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyUp(int keyCode, KeyEvent event) { michael@0: return processKey(keyCode, event, false); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) { michael@0: if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { michael@0: // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters() michael@0: View view = getView(); michael@0: if (view != null) { michael@0: InputThreadUtils.sInstance.runOnIcThread( michael@0: view.getRootView().getHandler(), mEditableClient, michael@0: new Runnable() { michael@0: @Override public void run() { michael@0: // Don't call GeckoInputConnection.commitText because it can michael@0: // post a key event back to onKeyMultiple, causing a loop michael@0: GeckoInputConnection.super.commitText(event.getCharacters(), 1); michael@0: } michael@0: }); michael@0: } michael@0: return true; michael@0: } michael@0: while ((repeatCount--) != 0) { michael@0: if (!processKey(keyCode, event, true) || michael@0: !processKey(keyCode, event, false)) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyLongPress(int keyCode, KeyEvent event) { michael@0: View v = getView(); michael@0: switch (keyCode) { michael@0: case KeyEvent.KEYCODE_MENU: michael@0: InputMethodManager imm = getInputMethodManager(); michael@0: imm.toggleSoftInputFromWindow(v.getWindowToken(), michael@0: InputMethodManager.SHOW_FORCED, 0); michael@0: return true; michael@0: default: michael@0: break; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public boolean isIMEEnabled() { michael@0: // make sure this picks up PASSWORD and PLUGIN states as well michael@0: return mIMEState != IME_STATE_DISABLED; michael@0: } michael@0: michael@0: @Override michael@0: public void notifyIME(int type) { michael@0: switch (type) { michael@0: michael@0: case NOTIFY_IME_TO_CANCEL_COMPOSITION: michael@0: // Set composition to empty and end composition michael@0: setComposingText("", 0); michael@0: // Fall through michael@0: michael@0: case NOTIFY_IME_TO_COMMIT_COMPOSITION: michael@0: // Commit and end composition michael@0: finishComposingText(); michael@0: tryRestartInput(); michael@0: break; michael@0: michael@0: case NOTIFY_IME_OF_FOCUS: michael@0: case NOTIFY_IME_OF_BLUR: michael@0: // Showing/hiding vkb is done in notifyIMEContext michael@0: resetInputConnection(); michael@0: break; michael@0: michael@0: case NOTIFY_IME_OPEN_VKB: michael@0: showSoftInput(); michael@0: break; michael@0: michael@0: default: michael@0: if (DEBUG) { michael@0: throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) { michael@0: // For some input type we will use a widget to display the ui, for those we must not michael@0: // display the ime. We can display a widget for date and time types and, if the sdk version michael@0: // is 11 or greater, for datetime/month/week as well. michael@0: if (typeHint != null && michael@0: (typeHint.equalsIgnoreCase("date") || michael@0: typeHint.equalsIgnoreCase("time") || michael@0: (Build.VERSION.SDK_INT >= 11 && (typeHint.equalsIgnoreCase("datetime") || michael@0: typeHint.equalsIgnoreCase("month") || michael@0: typeHint.equalsIgnoreCase("week") || michael@0: typeHint.equalsIgnoreCase("datetime-local"))))) { michael@0: state = IME_STATE_DISABLED; michael@0: } michael@0: michael@0: // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext, michael@0: // and not reset anywhere else. Usually, notifyIMEContext is called right after a michael@0: // focus or blur, so resetting mIMEState during the focus or blur seems harmless. michael@0: // However, this behavior is not guaranteed. Gecko may call notifyIMEContext michael@0: // independent of focus change; that is, a focus change may not be accompanied by michael@0: // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not michael@0: // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318) michael@0: /* When IME is 'disabled', IME processing is disabled. michael@0: In addition, the IME UI is hidden */ michael@0: mIMEState = state; michael@0: mIMETypeHint = (typeHint == null) ? "" : typeHint; michael@0: mIMEModeHint = (modeHint == null) ? "" : modeHint; michael@0: mIMEActionHint = (actionHint == null) ? "" : actionHint; michael@0: michael@0: // These fields are reset here and will be updated when restartInput is called below michael@0: mUpdateRequest = null; michael@0: mCurrentInputMethod = ""; michael@0: michael@0: View v = getView(); michael@0: if (v == null || !v.hasFocus()) { michael@0: // When using Find In Page, we can still receive notifyIMEContext calls due to the michael@0: // selection changing when highlighting. However in this case we don't want to reset/ michael@0: // show/hide the keyboard because the find box has the focus and is taking input from michael@0: // the keyboard. michael@0: return; michael@0: } michael@0: restartInput(); michael@0: if (mIMEState == IME_STATE_DISABLED) { michael@0: hideSoftInput(); michael@0: } else { michael@0: showSoftInput(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: final class DebugGeckoInputConnection michael@0: extends GeckoInputConnection michael@0: implements InvocationHandler { michael@0: michael@0: private InputConnection mProxy; michael@0: private StringBuilder mCallLevel; michael@0: michael@0: private DebugGeckoInputConnection(View targetView, michael@0: GeckoEditableClient editable) { michael@0: super(targetView, editable); michael@0: mCallLevel = new StringBuilder(); michael@0: } michael@0: michael@0: public static GeckoEditableListener create(View targetView, michael@0: GeckoEditableClient editable) { michael@0: final Class[] PROXY_INTERFACES = { InputConnection.class, michael@0: InputConnectionHandler.class, michael@0: GeckoEditableListener.class }; michael@0: DebugGeckoInputConnection dgic = michael@0: new DebugGeckoInputConnection(targetView, editable); michael@0: dgic.mProxy = (InputConnection)Proxy.newProxyInstance( michael@0: GeckoInputConnection.class.getClassLoader(), michael@0: PROXY_INTERFACES, dgic); michael@0: return (GeckoEditableListener)dgic.mProxy; michael@0: } michael@0: michael@0: @Override michael@0: public Object invoke(Object proxy, Method method, Object[] args) michael@0: throws Throwable { michael@0: michael@0: StringBuilder log = new StringBuilder(mCallLevel); michael@0: log.append("> ").append(method.getName()).append("("); michael@0: for (Object arg : args) { michael@0: // translate argument values to constant names michael@0: if ("notifyIME".equals(method.getName()) && arg == args[0]) { michael@0: log.append(GeckoEditable.getConstantName( michael@0: GeckoEditableListener.class, "NOTIFY_IME_", arg)); michael@0: } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) { michael@0: log.append(GeckoEditable.getConstantName( michael@0: GeckoEditableListener.class, "IME_STATE_", arg)); michael@0: } else { michael@0: GeckoEditable.debugAppend(log, arg); michael@0: } michael@0: log.append(", "); michael@0: } michael@0: if (args.length > 0) { michael@0: log.setLength(log.length() - 2); michael@0: } michael@0: log.append(")"); michael@0: Log.d(LOGTAG, log.toString()); michael@0: michael@0: mCallLevel.append(' '); michael@0: Object ret = method.invoke(this, args); michael@0: if (ret == this) { michael@0: ret = mProxy; michael@0: } michael@0: mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1)); michael@0: michael@0: log.setLength(mCallLevel.length()); michael@0: log.append("< ").append(method.getName()); michael@0: if (!method.getReturnType().equals(Void.TYPE)) { michael@0: GeckoEditable.debugAppend(log.append(": "), ret); michael@0: } michael@0: Log.d(LOGTAG, log.toString()); michael@0: return ret; michael@0: } michael@0: }