1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/GeckoInputConnection.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1064 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import java.lang.reflect.InvocationHandler; 1.12 +import java.lang.reflect.Method; 1.13 +import java.lang.reflect.Proxy; 1.14 +import java.util.concurrent.SynchronousQueue; 1.15 + 1.16 +import org.mozilla.gecko.gfx.InputConnectionHandler; 1.17 +import org.mozilla.gecko.util.Clipboard; 1.18 +import org.mozilla.gecko.util.GamepadUtils; 1.19 +import org.mozilla.gecko.util.ThreadUtils; 1.20 +import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; 1.21 + 1.22 +import android.R; 1.23 +import android.content.Context; 1.24 +import android.os.Build; 1.25 +import android.os.Handler; 1.26 +import android.os.Looper; 1.27 +import android.os.SystemClock; 1.28 +import android.text.Editable; 1.29 +import android.text.InputType; 1.30 +import android.text.Selection; 1.31 +import android.text.SpannableString; 1.32 +import android.text.method.KeyListener; 1.33 +import android.text.method.TextKeyListener; 1.34 +import android.util.DisplayMetrics; 1.35 +import android.util.Log; 1.36 +import android.view.KeyEvent; 1.37 +import android.view.View; 1.38 +import android.view.inputmethod.BaseInputConnection; 1.39 +import android.view.inputmethod.EditorInfo; 1.40 +import android.view.inputmethod.ExtractedText; 1.41 +import android.view.inputmethod.ExtractedTextRequest; 1.42 +import android.view.inputmethod.InputConnection; 1.43 +import android.view.inputmethod.InputMethodManager; 1.44 + 1.45 +class GeckoInputConnection 1.46 + extends BaseInputConnection 1.47 + implements InputConnectionHandler, GeckoEditableListener { 1.48 + 1.49 + private static final boolean DEBUG = false; 1.50 + protected static final String LOGTAG = "GeckoInputConnection"; 1.51 + 1.52 + private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection"; 1.53 + private static final String CUSTOM_HANDLER_TEST_CLASS = 1.54 + "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput"; 1.55 + 1.56 + private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480; 1.57 + 1.58 + private static Handler sBackgroundHandler; 1.59 + 1.60 + private static class InputThreadUtils { 1.61 + // We only want one UI editable around to keep synchronization simple, 1.62 + // so we make InputThreadUtils a singleton 1.63 + public static final InputThreadUtils sInstance = new InputThreadUtils(); 1.64 + 1.65 + private Editable mUiEditable; 1.66 + private Object mUiEditableReturn; 1.67 + private Exception mUiEditableException; 1.68 + private final SynchronousQueue<Runnable> mIcRunnableSync; 1.69 + private final Runnable mIcSignalRunnable; 1.70 + 1.71 + private InputThreadUtils() { 1.72 + mIcRunnableSync = new SynchronousQueue<Runnable>(); 1.73 + mIcSignalRunnable = new Runnable() { 1.74 + @Override public void run() { 1.75 + } 1.76 + }; 1.77 + } 1.78 + 1.79 + private void runOnIcThread(Handler icHandler, final Runnable runnable) { 1.80 + if (DEBUG) { 1.81 + ThreadUtils.assertOnUiThread(); 1.82 + Log.d(LOGTAG, "runOnIcThread() on thread " + 1.83 + icHandler.getLooper().getThread().getName()); 1.84 + } 1.85 + Runnable runner = new Runnable() { 1.86 + @Override public void run() { 1.87 + try { 1.88 + Runnable queuedRunnable = mIcRunnableSync.take(); 1.89 + if (DEBUG && queuedRunnable != runnable) { 1.90 + throw new IllegalThreadStateException("sync error"); 1.91 + } 1.92 + queuedRunnable.run(); 1.93 + } catch (InterruptedException e) { 1.94 + } 1.95 + } 1.96 + }; 1.97 + try { 1.98 + // if we are not inside waitForUiThread(), runner will call the runnable 1.99 + icHandler.post(runner); 1.100 + // runnable will be called by either runner from above or waitForUiThread() 1.101 + mIcRunnableSync.put(runnable); 1.102 + } catch (InterruptedException e) { 1.103 + } finally { 1.104 + // if waitForUiThread() already called runnable, runner should not call it again 1.105 + icHandler.removeCallbacks(runner); 1.106 + } 1.107 + } 1.108 + 1.109 + public void endWaitForUiThread() { 1.110 + if (DEBUG) { 1.111 + ThreadUtils.assertOnUiThread(); 1.112 + Log.d(LOGTAG, "endWaitForUiThread()"); 1.113 + } 1.114 + try { 1.115 + mIcRunnableSync.put(mIcSignalRunnable); 1.116 + } catch (InterruptedException e) { 1.117 + } 1.118 + } 1.119 + 1.120 + public void waitForUiThread(Handler icHandler) { 1.121 + if (DEBUG) { 1.122 + ThreadUtils.assertOnThread(icHandler.getLooper().getThread(), AssertBehavior.THROW); 1.123 + Log.d(LOGTAG, "waitForUiThread() blocking on thread " + 1.124 + icHandler.getLooper().getThread().getName()); 1.125 + } 1.126 + try { 1.127 + Runnable runnable = null; 1.128 + do { 1.129 + runnable = mIcRunnableSync.take(); 1.130 + runnable.run(); 1.131 + } while (runnable != mIcSignalRunnable); 1.132 + } catch (InterruptedException e) { 1.133 + } 1.134 + } 1.135 + 1.136 + public void runOnIcThread(final Handler uiHandler, 1.137 + final GeckoEditableClient client, 1.138 + final Runnable runnable) { 1.139 + final Handler icHandler = client.getInputConnectionHandler(); 1.140 + if (icHandler.getLooper() == uiHandler.getLooper()) { 1.141 + // IC thread is UI thread; safe to run directly 1.142 + runnable.run(); 1.143 + return; 1.144 + } 1.145 + runOnIcThread(icHandler, runnable); 1.146 + } 1.147 + 1.148 + public void sendEventFromUiThread(final Handler uiHandler, 1.149 + final GeckoEditableClient client, 1.150 + final GeckoEvent event) { 1.151 + runOnIcThread(uiHandler, client, new Runnable() { 1.152 + @Override public void run() { 1.153 + client.sendEvent(event); 1.154 + } 1.155 + }); 1.156 + } 1.157 + 1.158 + public Editable getEditableForUiThread(final Handler uiHandler, 1.159 + final GeckoEditableClient client) { 1.160 + if (DEBUG) { 1.161 + ThreadUtils.assertOnThread(uiHandler.getLooper().getThread(), AssertBehavior.THROW); 1.162 + } 1.163 + final Handler icHandler = client.getInputConnectionHandler(); 1.164 + if (icHandler.getLooper() == uiHandler.getLooper()) { 1.165 + // IC thread is UI thread; safe to use Editable directly 1.166 + return client.getEditable(); 1.167 + } 1.168 + // IC thread is not UI thread; we need to return a proxy Editable in order 1.169 + // to safely use the Editable from the UI thread 1.170 + if (mUiEditable != null) { 1.171 + return mUiEditable; 1.172 + } 1.173 + final InvocationHandler invokeEditable = new InvocationHandler() { 1.174 + @Override public Object invoke(final Object proxy, 1.175 + final Method method, 1.176 + final Object[] args) throws Throwable { 1.177 + if (DEBUG) { 1.178 + ThreadUtils.assertOnThread(uiHandler.getLooper().getThread(), AssertBehavior.THROW); 1.179 + Log.d(LOGTAG, "UiEditable." + method.getName() + "() blocking"); 1.180 + } 1.181 + synchronized (icHandler) { 1.182 + // Now we are on UI thread 1.183 + mUiEditableReturn = null; 1.184 + mUiEditableException = null; 1.185 + // Post a Runnable that calls the real Editable and saves any 1.186 + // result/exception. Then wait on the Runnable to finish 1.187 + runOnIcThread(icHandler, new Runnable() { 1.188 + @Override public void run() { 1.189 + synchronized (icHandler) { 1.190 + try { 1.191 + mUiEditableReturn = method.invoke( 1.192 + client.getEditable(), args); 1.193 + } catch (Exception e) { 1.194 + mUiEditableException = e; 1.195 + } 1.196 + if (DEBUG) { 1.197 + Log.d(LOGTAG, "UiEditable." + method.getName() + 1.198 + "() returning"); 1.199 + } 1.200 + icHandler.notify(); 1.201 + } 1.202 + } 1.203 + }); 1.204 + // let InterruptedException propagate 1.205 + icHandler.wait(); 1.206 + if (mUiEditableException != null) { 1.207 + throw mUiEditableException; 1.208 + } 1.209 + return mUiEditableReturn; 1.210 + } 1.211 + } 1.212 + }; 1.213 + mUiEditable = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(), 1.214 + new Class<?>[] { Editable.class }, invokeEditable); 1.215 + return mUiEditable; 1.216 + } 1.217 + } 1.218 + 1.219 + // Managed only by notifyIMEContext; see comments in notifyIMEContext 1.220 + private int mIMEState; 1.221 + private String mIMETypeHint = ""; 1.222 + private String mIMEModeHint = ""; 1.223 + private String mIMEActionHint = ""; 1.224 + 1.225 + private String mCurrentInputMethod = ""; 1.226 + 1.227 + private final GeckoEditableClient mEditableClient; 1.228 + protected int mBatchEditCount; 1.229 + private ExtractedTextRequest mUpdateRequest; 1.230 + private final ExtractedText mUpdateExtract = new ExtractedText(); 1.231 + private boolean mBatchSelectionChanged; 1.232 + private boolean mBatchTextChanged; 1.233 + private long mLastRestartInputTime; 1.234 + private final InputConnection mKeyInputConnection; 1.235 + 1.236 + public static GeckoEditableListener create(View targetView, 1.237 + GeckoEditableClient editable) { 1.238 + if (DEBUG) 1.239 + return DebugGeckoInputConnection.create(targetView, editable); 1.240 + else 1.241 + return new GeckoInputConnection(targetView, editable); 1.242 + } 1.243 + 1.244 + protected GeckoInputConnection(View targetView, 1.245 + GeckoEditableClient editable) { 1.246 + super(targetView, true); 1.247 + mEditableClient = editable; 1.248 + mIMEState = IME_STATE_DISABLED; 1.249 + // InputConnection that sends keys for plugins, which don't have full editors 1.250 + mKeyInputConnection = new BaseInputConnection(targetView, false); 1.251 + } 1.252 + 1.253 + @Override 1.254 + public synchronized boolean beginBatchEdit() { 1.255 + mBatchEditCount++; 1.256 + mEditableClient.setUpdateGecko(false); 1.257 + return true; 1.258 + } 1.259 + 1.260 + @Override 1.261 + public synchronized boolean endBatchEdit() { 1.262 + if (mBatchEditCount > 0) { 1.263 + mBatchEditCount--; 1.264 + if (mBatchEditCount == 0) { 1.265 + if (mBatchTextChanged) { 1.266 + notifyTextChange(); 1.267 + mBatchTextChanged = false; 1.268 + } 1.269 + if (mBatchSelectionChanged) { 1.270 + Editable editable = getEditable(); 1.271 + notifySelectionChange(Selection.getSelectionStart(editable), 1.272 + Selection.getSelectionEnd(editable)); 1.273 + mBatchSelectionChanged = false; 1.274 + } 1.275 + mEditableClient.setUpdateGecko(true); 1.276 + } 1.277 + } else { 1.278 + Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!"); 1.279 + } 1.280 + return true; 1.281 + } 1.282 + 1.283 + @Override 1.284 + public Editable getEditable() { 1.285 + return mEditableClient.getEditable(); 1.286 + } 1.287 + 1.288 + @Override 1.289 + public boolean performContextMenuAction(int id) { 1.290 + Editable editable = getEditable(); 1.291 + if (editable == null) { 1.292 + return false; 1.293 + } 1.294 + int selStart = Selection.getSelectionStart(editable); 1.295 + int selEnd = Selection.getSelectionEnd(editable); 1.296 + 1.297 + switch (id) { 1.298 + case R.id.selectAll: 1.299 + setSelection(0, editable.length()); 1.300 + break; 1.301 + case R.id.cut: 1.302 + // If selection is empty, we'll select everything 1.303 + if (selStart == selEnd) { 1.304 + // Fill the clipboard 1.305 + Clipboard.setText(editable); 1.306 + editable.clear(); 1.307 + } else { 1.308 + Clipboard.setText( 1.309 + editable.toString().substring( 1.310 + Math.min(selStart, selEnd), 1.311 + Math.max(selStart, selEnd))); 1.312 + editable.delete(selStart, selEnd); 1.313 + } 1.314 + break; 1.315 + case R.id.paste: 1.316 + commitText(Clipboard.getText(), 1); 1.317 + break; 1.318 + case R.id.copy: 1.319 + // Copy the current selection or the empty string if nothing is selected. 1.320 + String copiedText = selStart == selEnd ? "" : 1.321 + editable.toString().substring( 1.322 + Math.min(selStart, selEnd), 1.323 + Math.max(selStart, selEnd)); 1.324 + Clipboard.setText(copiedText); 1.325 + break; 1.326 + } 1.327 + return true; 1.328 + } 1.329 + 1.330 + @Override 1.331 + public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) { 1.332 + if (req == null) 1.333 + return null; 1.334 + 1.335 + if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) 1.336 + mUpdateRequest = req; 1.337 + 1.338 + Editable editable = getEditable(); 1.339 + if (editable == null) { 1.340 + return null; 1.341 + } 1.342 + int selStart = Selection.getSelectionStart(editable); 1.343 + int selEnd = Selection.getSelectionEnd(editable); 1.344 + 1.345 + ExtractedText extract = new ExtractedText(); 1.346 + extract.flags = 0; 1.347 + extract.partialStartOffset = -1; 1.348 + extract.partialEndOffset = -1; 1.349 + extract.selectionStart = selStart; 1.350 + extract.selectionEnd = selEnd; 1.351 + extract.startOffset = 0; 1.352 + if ((req.flags & GET_TEXT_WITH_STYLES) != 0) { 1.353 + extract.text = new SpannableString(editable); 1.354 + } else { 1.355 + extract.text = editable.toString(); 1.356 + } 1.357 + return extract; 1.358 + } 1.359 + 1.360 + private static View getView() { 1.361 + return GeckoAppShell.getLayerView(); 1.362 + } 1.363 + 1.364 + private static InputMethodManager getInputMethodManager() { 1.365 + View view = getView(); 1.366 + if (view == null) { 1.367 + return null; 1.368 + } 1.369 + Context context = view.getContext(); 1.370 + return InputMethods.getInputMethodManager(context); 1.371 + } 1.372 + 1.373 + private static void showSoftInput() { 1.374 + final InputMethodManager imm = getInputMethodManager(); 1.375 + if (imm != null) { 1.376 + final View v = getView(); 1.377 + imm.showSoftInput(v, 0); 1.378 + } 1.379 + } 1.380 + 1.381 + private static void hideSoftInput() { 1.382 + final InputMethodManager imm = getInputMethodManager(); 1.383 + if (imm != null) { 1.384 + final View v = getView(); 1.385 + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); 1.386 + } 1.387 + } 1.388 + 1.389 + private void tryRestartInput() { 1.390 + // Coalesce restartInput calls because InputMethodManager.restartInput() 1.391 + // is expensive and successive calls to it can lock up the keyboard 1.392 + if (SystemClock.uptimeMillis() < mLastRestartInputTime + 200) { 1.393 + return; 1.394 + } 1.395 + restartInput(); 1.396 + } 1.397 + 1.398 + private void restartInput() { 1.399 + 1.400 + mLastRestartInputTime = SystemClock.uptimeMillis(); 1.401 + 1.402 + final InputMethodManager imm = getInputMethodManager(); 1.403 + if (imm == null) { 1.404 + return; 1.405 + } 1.406 + final View v = getView(); 1.407 + // InputMethodManager has internal logic to detect if we are restarting input 1.408 + // in an already focused View, which is the case here because all content text 1.409 + // fields are inside one LayerView. When this happens, InputMethodManager will 1.410 + // tell the input method to soft reset instead of hard reset. Stock latin IME 1.411 + // on Android 4.2+ has a quirk that when it soft resets, it does not clear the 1.412 + // composition. The following workaround tricks the IME into clearing the 1.413 + // composition when soft resetting. 1.414 + if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) { 1.415 + // Fake a selection change, because the IME clears the composition when 1.416 + // the selection changes, even if soft-resetting. Offsets here must be 1.417 + // different from the previous selection offsets, and -1 seems to be a 1.418 + // reasonable, deterministic value 1.419 + notifySelectionChange(-1, -1); 1.420 + } 1.421 + imm.restartInput(v); 1.422 + } 1.423 + 1.424 + private void resetInputConnection() { 1.425 + if (mBatchEditCount != 0) { 1.426 + Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount); 1.427 + mBatchEditCount = 0; 1.428 + } 1.429 + mBatchSelectionChanged = false; 1.430 + mBatchTextChanged = false; 1.431 + 1.432 + // Do not reset mIMEState here; see comments in notifyIMEContext 1.433 + } 1.434 + 1.435 + @Override 1.436 + public void onTextChange(String text, int start, int oldEnd, int newEnd) { 1.437 + 1.438 + if (mUpdateRequest == null) { 1.439 + // Android always expects selection updates when not in extracted mode; 1.440 + // in extracted mode, the selection is reported through updateExtractedText 1.441 + final Editable editable = getEditable(); 1.442 + if (editable != null) { 1.443 + onSelectionChange(Selection.getSelectionStart(editable), 1.444 + Selection.getSelectionEnd(editable)); 1.445 + } 1.446 + return; 1.447 + } 1.448 + 1.449 + if (mBatchEditCount > 0) { 1.450 + // Delay notification until after the batch edit 1.451 + mBatchTextChanged = true; 1.452 + return; 1.453 + } 1.454 + notifyTextChange(); 1.455 + } 1.456 + 1.457 + private void notifyTextChange() { 1.458 + 1.459 + final InputMethodManager imm = getInputMethodManager(); 1.460 + final View v = getView(); 1.461 + final Editable editable = getEditable(); 1.462 + if (imm == null || v == null || editable == null) { 1.463 + return; 1.464 + } 1.465 + mUpdateExtract.flags = 0; 1.466 + // Update the entire Editable range 1.467 + mUpdateExtract.partialStartOffset = -1; 1.468 + mUpdateExtract.partialEndOffset = -1; 1.469 + mUpdateExtract.selectionStart = 1.470 + Selection.getSelectionStart(editable); 1.471 + mUpdateExtract.selectionEnd = 1.472 + Selection.getSelectionEnd(editable); 1.473 + mUpdateExtract.startOffset = 0; 1.474 + if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) { 1.475 + mUpdateExtract.text = new SpannableString(editable); 1.476 + } else { 1.477 + mUpdateExtract.text = editable.toString(); 1.478 + } 1.479 + imm.updateExtractedText(v, mUpdateRequest.token, 1.480 + mUpdateExtract); 1.481 + } 1.482 + 1.483 + @Override 1.484 + public void onSelectionChange(int start, int end) { 1.485 + 1.486 + if (mBatchEditCount > 0) { 1.487 + // Delay notification until after the batch edit 1.488 + mBatchSelectionChanged = true; 1.489 + return; 1.490 + } 1.491 + notifySelectionChange(start, end); 1.492 + } 1.493 + 1.494 + private void notifySelectionChange(int start, int end) { 1.495 + 1.496 + final InputMethodManager imm = getInputMethodManager(); 1.497 + final View v = getView(); 1.498 + final Editable editable = getEditable(); 1.499 + if (imm == null || v == null || editable == null) { 1.500 + return; 1.501 + } 1.502 + imm.updateSelection(v, start, end, getComposingSpanStart(editable), 1.503 + getComposingSpanEnd(editable)); 1.504 + } 1.505 + 1.506 + private static synchronized Handler getBackgroundHandler() { 1.507 + if (sBackgroundHandler != null) { 1.508 + return sBackgroundHandler; 1.509 + } 1.510 + // Don't use GeckoBackgroundThread because Gecko thread may block waiting on 1.511 + // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME, 1.512 + // GeckoBackgroundThread may end up also block waiting on Gecko thread and a 1.513 + // deadlock occurs 1.514 + Thread backgroundThread = new Thread(new Runnable() { 1.515 + @Override 1.516 + public void run() { 1.517 + Looper.prepare(); 1.518 + synchronized (GeckoInputConnection.class) { 1.519 + sBackgroundHandler = new Handler(); 1.520 + GeckoInputConnection.class.notify(); 1.521 + } 1.522 + Looper.loop(); 1.523 + sBackgroundHandler = null; 1.524 + } 1.525 + }, LOGTAG); 1.526 + backgroundThread.setDaemon(true); 1.527 + backgroundThread.start(); 1.528 + while (sBackgroundHandler == null) { 1.529 + try { 1.530 + // wait for new thread to set sBackgroundHandler 1.531 + GeckoInputConnection.class.wait(); 1.532 + } catch (InterruptedException e) { 1.533 + } 1.534 + } 1.535 + return sBackgroundHandler; 1.536 + } 1.537 + 1.538 + private boolean canReturnCustomHandler() { 1.539 + if (mIMEState == IME_STATE_DISABLED) { 1.540 + return false; 1.541 + } 1.542 + for (StackTraceElement frame : Thread.currentThread().getStackTrace()) { 1.543 + // We only return our custom Handler to InputMethodManager's InputConnection 1.544 + // proxy. For all other purposes, we return the regular Handler. 1.545 + // InputMethodManager retrieves the Handler for its InputConnection proxy 1.546 + // inside its method startInputInner(), so we check for that here. This is 1.547 + // valid from Android 2.2 to at least Android 4.2. If this situation ever 1.548 + // changes, we gracefully fall back to using the regular Handler. 1.549 + if ("startInputInner".equals(frame.getMethodName()) && 1.550 + "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) { 1.551 + // only return our own Handler to InputMethodManager 1.552 + return true; 1.553 + } 1.554 + if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) && 1.555 + CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) { 1.556 + // InputConnection tests should also run on the custom handler 1.557 + return true; 1.558 + } 1.559 + } 1.560 + return false; 1.561 + } 1.562 + 1.563 + @Override 1.564 + public Handler getHandler(Handler defHandler) { 1.565 + if (!canReturnCustomHandler()) { 1.566 + return defHandler; 1.567 + } 1.568 + // getBackgroundHandler() is synchronized and requires locking, 1.569 + // but if we already have our handler, we don't have to lock 1.570 + final Handler newHandler = sBackgroundHandler != null 1.571 + ? sBackgroundHandler 1.572 + : getBackgroundHandler(); 1.573 + if (mEditableClient.setInputConnectionHandler(newHandler)) { 1.574 + return newHandler; 1.575 + } 1.576 + // Setting new IC handler failed; return old IC handler 1.577 + return mEditableClient.getInputConnectionHandler(); 1.578 + } 1.579 + 1.580 + @Override 1.581 + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 1.582 + if (mIMEState == IME_STATE_DISABLED) { 1.583 + return null; 1.584 + } 1.585 + 1.586 + outAttrs.inputType = InputType.TYPE_CLASS_TEXT; 1.587 + outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE; 1.588 + outAttrs.actionLabel = null; 1.589 + 1.590 + if (mIMEState == IME_STATE_PASSWORD || 1.591 + "password".equalsIgnoreCase(mIMETypeHint)) 1.592 + outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; 1.593 + else if (mIMEState == IME_STATE_PLUGIN) 1.594 + outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode 1.595 + else if (mIMETypeHint.equalsIgnoreCase("url")) 1.596 + outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI; 1.597 + else if (mIMETypeHint.equalsIgnoreCase("email")) 1.598 + outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; 1.599 + else if (mIMETypeHint.equalsIgnoreCase("search")) 1.600 + outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; 1.601 + else if (mIMETypeHint.equalsIgnoreCase("tel")) 1.602 + outAttrs.inputType = InputType.TYPE_CLASS_PHONE; 1.603 + else if (mIMETypeHint.equalsIgnoreCase("number") || 1.604 + mIMETypeHint.equalsIgnoreCase("range")) 1.605 + outAttrs.inputType = InputType.TYPE_CLASS_NUMBER 1.606 + | InputType.TYPE_NUMBER_FLAG_SIGNED 1.607 + | InputType.TYPE_NUMBER_FLAG_DECIMAL; 1.608 + else if (mIMETypeHint.equalsIgnoreCase("week") || 1.609 + mIMETypeHint.equalsIgnoreCase("month")) 1.610 + outAttrs.inputType = InputType.TYPE_CLASS_DATETIME 1.611 + | InputType.TYPE_DATETIME_VARIATION_DATE; 1.612 + else if (mIMEModeHint.equalsIgnoreCase("numeric")) 1.613 + outAttrs.inputType = InputType.TYPE_CLASS_NUMBER | 1.614 + InputType.TYPE_NUMBER_FLAG_SIGNED | 1.615 + InputType.TYPE_NUMBER_FLAG_DECIMAL; 1.616 + else if (mIMEModeHint.equalsIgnoreCase("digit")) 1.617 + outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; 1.618 + else { 1.619 + // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap 1.620 + outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | 1.621 + InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE; 1.622 + if (mIMETypeHint.equalsIgnoreCase("textarea") || 1.623 + mIMETypeHint.length() == 0) { 1.624 + // empty mIMETypeHint indicates contentEditable/designMode documents 1.625 + outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; 1.626 + } 1.627 + if (mIMEModeHint.equalsIgnoreCase("uppercase")) 1.628 + outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; 1.629 + else if (mIMEModeHint.equalsIgnoreCase("titlecase")) 1.630 + outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; 1.631 + else if (!mIMEModeHint.equalsIgnoreCase("lowercase")) 1.632 + outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 1.633 + // auto-capitalized mode is the default 1.634 + } 1.635 + 1.636 + if (mIMEActionHint.equalsIgnoreCase("go")) 1.637 + outAttrs.imeOptions = EditorInfo.IME_ACTION_GO; 1.638 + else if (mIMEActionHint.equalsIgnoreCase("done")) 1.639 + outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; 1.640 + else if (mIMEActionHint.equalsIgnoreCase("next")) 1.641 + outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; 1.642 + else if (mIMEActionHint.equalsIgnoreCase("search")) 1.643 + outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH; 1.644 + else if (mIMEActionHint.equalsIgnoreCase("send")) 1.645 + outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND; 1.646 + else if (mIMEActionHint.length() > 0) { 1.647 + if (DEBUG) 1.648 + Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\""); 1.649 + outAttrs.actionLabel = mIMEActionHint; 1.650 + } 1.651 + 1.652 + Context context = GeckoAppShell.getContext(); 1.653 + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 1.654 + if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) { 1.655 + // prevent showing full-screen keyboard only when the screen is tall enough 1.656 + // to show some reasonable amount of the page (see bug 752709) 1.657 + outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI 1.658 + | EditorInfo.IME_FLAG_NO_FULLSCREEN; 1.659 + } 1.660 + 1.661 + if (DEBUG) { 1.662 + Log.d(LOGTAG, "mapped IME states to: inputType = " + 1.663 + Integer.toHexString(outAttrs.inputType) + ", imeOptions = " + 1.664 + Integer.toHexString(outAttrs.imeOptions)); 1.665 + } 1.666 + 1.667 + String prevInputMethod = mCurrentInputMethod; 1.668 + mCurrentInputMethod = InputMethods.getCurrentInputMethod(context); 1.669 + if (DEBUG) { 1.670 + Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod); 1.671 + } 1.672 + 1.673 + // If the user has changed IMEs, then notify input method observers. 1.674 + if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) { 1.675 + FormAssistPopup popup = GeckoAppShell.getGeckoInterface().getFormAssistPopup(); 1.676 + if (popup != null) { 1.677 + popup.onInputMethodChanged(mCurrentInputMethod); 1.678 + } 1.679 + } 1.680 + 1.681 + if (mIMEState == IME_STATE_PLUGIN) { 1.682 + // Since we are using a temporary string as the editable, the selection is at 0 1.683 + outAttrs.initialSelStart = 0; 1.684 + outAttrs.initialSelEnd = 0; 1.685 + return mKeyInputConnection; 1.686 + } 1.687 + Editable editable = getEditable(); 1.688 + outAttrs.initialSelStart = Selection.getSelectionStart(editable); 1.689 + outAttrs.initialSelEnd = Selection.getSelectionEnd(editable); 1.690 + return this; 1.691 + } 1.692 + 1.693 + private boolean replaceComposingSpanWithSelection() { 1.694 + final Editable content = getEditable(); 1.695 + if (content == null) { 1.696 + return false; 1.697 + } 1.698 + int a = getComposingSpanStart(content), 1.699 + b = getComposingSpanEnd(content); 1.700 + if (a != -1 && b != -1) { 1.701 + if (DEBUG) { 1.702 + Log.d(LOGTAG, "removing composition at " + a + "-" + b); 1.703 + } 1.704 + removeComposingSpans(content); 1.705 + Selection.setSelection(content, a, b); 1.706 + } 1.707 + return true; 1.708 + } 1.709 + 1.710 + @Override 1.711 + public boolean commitText(CharSequence text, int newCursorPosition) { 1.712 + if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) && 1.713 + text.length() == 1 && newCursorPosition > 0) { 1.714 + if (DEBUG) { 1.715 + Log.d(LOGTAG, "committing \"" + text + "\" as key"); 1.716 + } 1.717 + // mKeyInputConnection is a BaseInputConnection that commits text as keys; 1.718 + // but we first need to replace any composing span with a selection, 1.719 + // so that the new key events will generate characters to replace 1.720 + // text from the old composing span 1.721 + return replaceComposingSpanWithSelection() && 1.722 + mKeyInputConnection.commitText(text, newCursorPosition); 1.723 + } 1.724 + return super.commitText(text, newCursorPosition); 1.725 + } 1.726 + 1.727 + @Override 1.728 + public boolean setSelection(int start, int end) { 1.729 + if (start < 0 || end < 0) { 1.730 + // Some keyboards (e.g. Samsung) can call setSelection with 1.731 + // negative offsets. In that case we ignore the call, similar to how 1.732 + // BaseInputConnection.setSelection ignores offsets that go past the length. 1.733 + return true; 1.734 + } 1.735 + return super.setSelection(start, end); 1.736 + } 1.737 + 1.738 + @Override 1.739 + public boolean sendKeyEvent(KeyEvent event) { 1.740 + // BaseInputConnection.sendKeyEvent() dispatches the key event to the main thread. 1.741 + // In order to ensure events are processed in the proper order, we must block the 1.742 + // IC thread until the main thread finishes processing the key event 1.743 + super.sendKeyEvent(event); 1.744 + final View v = getView(); 1.745 + if (v == null) { 1.746 + return false; 1.747 + } 1.748 + final Handler icHandler = mEditableClient.getInputConnectionHandler(); 1.749 + final Handler mainHandler = v.getRootView().getHandler(); 1.750 + if (icHandler.getLooper() != mainHandler.getLooper()) { 1.751 + // We are on separate IC thread but the event is queued on the main thread; 1.752 + // wait on IC thread until the main thread processes our posted Runnable. At 1.753 + // that point the key event has already been processed. 1.754 + mainHandler.post(new Runnable() { 1.755 + @Override public void run() { 1.756 + InputThreadUtils.sInstance.endWaitForUiThread(); 1.757 + } 1.758 + }); 1.759 + InputThreadUtils.sInstance.waitForUiThread(icHandler); 1.760 + } 1.761 + return false; // seems to always return false 1.762 + } 1.763 + 1.764 + @Override 1.765 + public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1.766 + return false; 1.767 + } 1.768 + 1.769 + private boolean shouldProcessKey(int keyCode, KeyEvent event) { 1.770 + switch (keyCode) { 1.771 + case KeyEvent.KEYCODE_MENU: 1.772 + case KeyEvent.KEYCODE_BACK: 1.773 + case KeyEvent.KEYCODE_VOLUME_UP: 1.774 + case KeyEvent.KEYCODE_VOLUME_DOWN: 1.775 + case KeyEvent.KEYCODE_SEARCH: 1.776 + return false; 1.777 + } 1.778 + return true; 1.779 + } 1.780 + 1.781 + private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) { 1.782 + if (mIMEState == IME_STATE_DISABLED || 1.783 + mIMEState == IME_STATE_PLUGIN) { 1.784 + return true; 1.785 + } 1.786 + // Preserve enter and tab keys for the browser 1.787 + if (keyCode == KeyEvent.KEYCODE_ENTER || 1.788 + keyCode == KeyEvent.KEYCODE_TAB) { 1.789 + return true; 1.790 + } 1.791 + // BaseKeyListener returns false even if it handled these keys for us, 1.792 + // so we skip the key listener entirely and handle these ourselves 1.793 + if (keyCode == KeyEvent.KEYCODE_DEL || 1.794 + keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { 1.795 + return true; 1.796 + } 1.797 + return false; 1.798 + } 1.799 + 1.800 + private KeyEvent translateKey(int keyCode, KeyEvent event) { 1.801 + switch (keyCode) { 1.802 + case KeyEvent.KEYCODE_ENTER: 1.803 + if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 && 1.804 + mIMEActionHint.equalsIgnoreCase("next")) { 1.805 + return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB); 1.806 + } 1.807 + break; 1.808 + } 1.809 + return event; 1.810 + } 1.811 + 1.812 + private boolean processKey(int keyCode, KeyEvent event, boolean down) { 1.813 + if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) { 1.814 + event = GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event); 1.815 + keyCode = event.getKeyCode(); 1.816 + } 1.817 + 1.818 + if (keyCode > KeyEvent.getMaxKeyCode() || 1.819 + !shouldProcessKey(keyCode, event)) { 1.820 + return false; 1.821 + } 1.822 + event = translateKey(keyCode, event); 1.823 + keyCode = event.getKeyCode(); 1.824 + 1.825 + View view = getView(); 1.826 + if (view == null) { 1.827 + InputThreadUtils.sInstance.sendEventFromUiThread(ThreadUtils.getUiHandler(), 1.828 + mEditableClient, GeckoEvent.createKeyEvent(event, 0)); 1.829 + return true; 1.830 + } 1.831 + 1.832 + // KeyListener returns true if it handled the event for us. KeyListener is only 1.833 + // safe to use on the UI thread; therefore we need to pass a proxy Editable to it 1.834 + KeyListener keyListener = TextKeyListener.getInstance(); 1.835 + Handler uiHandler = view.getRootView().getHandler(); 1.836 + Editable uiEditable = InputThreadUtils.sInstance. 1.837 + getEditableForUiThread(uiHandler, mEditableClient); 1.838 + boolean skip = shouldSkipKeyListener(keyCode, event); 1.839 + if (down) { 1.840 + mEditableClient.setSuppressKeyUp(true); 1.841 + } 1.842 + if (skip || 1.843 + (down && !keyListener.onKeyDown(view, uiEditable, keyCode, event)) || 1.844 + (!down && !keyListener.onKeyUp(view, uiEditable, keyCode, event))) { 1.845 + InputThreadUtils.sInstance.sendEventFromUiThread(uiHandler, mEditableClient, 1.846 + GeckoEvent.createKeyEvent(event, TextKeyListener.getMetaState(uiEditable))); 1.847 + if (skip && down) { 1.848 + // Usually, the down key listener call above adjusts meta states for us. 1.849 + // However, if we skip that call above, we have to manually adjust meta 1.850 + // states so the meta states remain consistent 1.851 + TextKeyListener.adjustMetaAfterKeypress(uiEditable); 1.852 + } 1.853 + } 1.854 + if (down) { 1.855 + mEditableClient.setSuppressKeyUp(false); 1.856 + } 1.857 + return true; 1.858 + } 1.859 + 1.860 + @Override 1.861 + public boolean onKeyDown(int keyCode, KeyEvent event) { 1.862 + return processKey(keyCode, event, true); 1.863 + } 1.864 + 1.865 + @Override 1.866 + public boolean onKeyUp(int keyCode, KeyEvent event) { 1.867 + return processKey(keyCode, event, false); 1.868 + } 1.869 + 1.870 + @Override 1.871 + public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) { 1.872 + if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { 1.873 + // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters() 1.874 + View view = getView(); 1.875 + if (view != null) { 1.876 + InputThreadUtils.sInstance.runOnIcThread( 1.877 + view.getRootView().getHandler(), mEditableClient, 1.878 + new Runnable() { 1.879 + @Override public void run() { 1.880 + // Don't call GeckoInputConnection.commitText because it can 1.881 + // post a key event back to onKeyMultiple, causing a loop 1.882 + GeckoInputConnection.super.commitText(event.getCharacters(), 1); 1.883 + } 1.884 + }); 1.885 + } 1.886 + return true; 1.887 + } 1.888 + while ((repeatCount--) != 0) { 1.889 + if (!processKey(keyCode, event, true) || 1.890 + !processKey(keyCode, event, false)) { 1.891 + return false; 1.892 + } 1.893 + } 1.894 + return true; 1.895 + } 1.896 + 1.897 + @Override 1.898 + public boolean onKeyLongPress(int keyCode, KeyEvent event) { 1.899 + View v = getView(); 1.900 + switch (keyCode) { 1.901 + case KeyEvent.KEYCODE_MENU: 1.902 + InputMethodManager imm = getInputMethodManager(); 1.903 + imm.toggleSoftInputFromWindow(v.getWindowToken(), 1.904 + InputMethodManager.SHOW_FORCED, 0); 1.905 + return true; 1.906 + default: 1.907 + break; 1.908 + } 1.909 + return false; 1.910 + } 1.911 + 1.912 + @Override 1.913 + public boolean isIMEEnabled() { 1.914 + // make sure this picks up PASSWORD and PLUGIN states as well 1.915 + return mIMEState != IME_STATE_DISABLED; 1.916 + } 1.917 + 1.918 + @Override 1.919 + public void notifyIME(int type) { 1.920 + switch (type) { 1.921 + 1.922 + case NOTIFY_IME_TO_CANCEL_COMPOSITION: 1.923 + // Set composition to empty and end composition 1.924 + setComposingText("", 0); 1.925 + // Fall through 1.926 + 1.927 + case NOTIFY_IME_TO_COMMIT_COMPOSITION: 1.928 + // Commit and end composition 1.929 + finishComposingText(); 1.930 + tryRestartInput(); 1.931 + break; 1.932 + 1.933 + case NOTIFY_IME_OF_FOCUS: 1.934 + case NOTIFY_IME_OF_BLUR: 1.935 + // Showing/hiding vkb is done in notifyIMEContext 1.936 + resetInputConnection(); 1.937 + break; 1.938 + 1.939 + case NOTIFY_IME_OPEN_VKB: 1.940 + showSoftInput(); 1.941 + break; 1.942 + 1.943 + default: 1.944 + if (DEBUG) { 1.945 + throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type); 1.946 + } 1.947 + break; 1.948 + } 1.949 + } 1.950 + 1.951 + @Override 1.952 + public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) { 1.953 + // For some input type we will use a widget to display the ui, for those we must not 1.954 + // display the ime. We can display a widget for date and time types and, if the sdk version 1.955 + // is 11 or greater, for datetime/month/week as well. 1.956 + if (typeHint != null && 1.957 + (typeHint.equalsIgnoreCase("date") || 1.958 + typeHint.equalsIgnoreCase("time") || 1.959 + (Build.VERSION.SDK_INT >= 11 && (typeHint.equalsIgnoreCase("datetime") || 1.960 + typeHint.equalsIgnoreCase("month") || 1.961 + typeHint.equalsIgnoreCase("week") || 1.962 + typeHint.equalsIgnoreCase("datetime-local"))))) { 1.963 + state = IME_STATE_DISABLED; 1.964 + } 1.965 + 1.966 + // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext, 1.967 + // and not reset anywhere else. Usually, notifyIMEContext is called right after a 1.968 + // focus or blur, so resetting mIMEState during the focus or blur seems harmless. 1.969 + // However, this behavior is not guaranteed. Gecko may call notifyIMEContext 1.970 + // independent of focus change; that is, a focus change may not be accompanied by 1.971 + // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not 1.972 + // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318) 1.973 + /* When IME is 'disabled', IME processing is disabled. 1.974 + In addition, the IME UI is hidden */ 1.975 + mIMEState = state; 1.976 + mIMETypeHint = (typeHint == null) ? "" : typeHint; 1.977 + mIMEModeHint = (modeHint == null) ? "" : modeHint; 1.978 + mIMEActionHint = (actionHint == null) ? "" : actionHint; 1.979 + 1.980 + // These fields are reset here and will be updated when restartInput is called below 1.981 + mUpdateRequest = null; 1.982 + mCurrentInputMethod = ""; 1.983 + 1.984 + View v = getView(); 1.985 + if (v == null || !v.hasFocus()) { 1.986 + // When using Find In Page, we can still receive notifyIMEContext calls due to the 1.987 + // selection changing when highlighting. However in this case we don't want to reset/ 1.988 + // show/hide the keyboard because the find box has the focus and is taking input from 1.989 + // the keyboard. 1.990 + return; 1.991 + } 1.992 + restartInput(); 1.993 + if (mIMEState == IME_STATE_DISABLED) { 1.994 + hideSoftInput(); 1.995 + } else { 1.996 + showSoftInput(); 1.997 + } 1.998 + } 1.999 +} 1.1000 + 1.1001 +final class DebugGeckoInputConnection 1.1002 + extends GeckoInputConnection 1.1003 + implements InvocationHandler { 1.1004 + 1.1005 + private InputConnection mProxy; 1.1006 + private StringBuilder mCallLevel; 1.1007 + 1.1008 + private DebugGeckoInputConnection(View targetView, 1.1009 + GeckoEditableClient editable) { 1.1010 + super(targetView, editable); 1.1011 + mCallLevel = new StringBuilder(); 1.1012 + } 1.1013 + 1.1014 + public static GeckoEditableListener create(View targetView, 1.1015 + GeckoEditableClient editable) { 1.1016 + final Class[] PROXY_INTERFACES = { InputConnection.class, 1.1017 + InputConnectionHandler.class, 1.1018 + GeckoEditableListener.class }; 1.1019 + DebugGeckoInputConnection dgic = 1.1020 + new DebugGeckoInputConnection(targetView, editable); 1.1021 + dgic.mProxy = (InputConnection)Proxy.newProxyInstance( 1.1022 + GeckoInputConnection.class.getClassLoader(), 1.1023 + PROXY_INTERFACES, dgic); 1.1024 + return (GeckoEditableListener)dgic.mProxy; 1.1025 + } 1.1026 + 1.1027 + @Override 1.1028 + public Object invoke(Object proxy, Method method, Object[] args) 1.1029 + throws Throwable { 1.1030 + 1.1031 + StringBuilder log = new StringBuilder(mCallLevel); 1.1032 + log.append("> ").append(method.getName()).append("("); 1.1033 + for (Object arg : args) { 1.1034 + // translate argument values to constant names 1.1035 + if ("notifyIME".equals(method.getName()) && arg == args[0]) { 1.1036 + log.append(GeckoEditable.getConstantName( 1.1037 + GeckoEditableListener.class, "NOTIFY_IME_", arg)); 1.1038 + } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) { 1.1039 + log.append(GeckoEditable.getConstantName( 1.1040 + GeckoEditableListener.class, "IME_STATE_", arg)); 1.1041 + } else { 1.1042 + GeckoEditable.debugAppend(log, arg); 1.1043 + } 1.1044 + log.append(", "); 1.1045 + } 1.1046 + if (args.length > 0) { 1.1047 + log.setLength(log.length() - 2); 1.1048 + } 1.1049 + log.append(")"); 1.1050 + Log.d(LOGTAG, log.toString()); 1.1051 + 1.1052 + mCallLevel.append(' '); 1.1053 + Object ret = method.invoke(this, args); 1.1054 + if (ret == this) { 1.1055 + ret = mProxy; 1.1056 + } 1.1057 + mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1)); 1.1058 + 1.1059 + log.setLength(mCallLevel.length()); 1.1060 + log.append("< ").append(method.getName()); 1.1061 + if (!method.getReturnType().equals(Void.TYPE)) { 1.1062 + GeckoEditable.debugAppend(log.append(": "), ret); 1.1063 + } 1.1064 + Log.d(LOGTAG, log.toString()); 1.1065 + return ret; 1.1066 + } 1.1067 +}