mobile/android/base/GeckoInputConnection.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko;
michael@0 7
michael@0 8 import java.lang.reflect.InvocationHandler;
michael@0 9 import java.lang.reflect.Method;
michael@0 10 import java.lang.reflect.Proxy;
michael@0 11 import java.util.concurrent.SynchronousQueue;
michael@0 12
michael@0 13 import org.mozilla.gecko.gfx.InputConnectionHandler;
michael@0 14 import org.mozilla.gecko.util.Clipboard;
michael@0 15 import org.mozilla.gecko.util.GamepadUtils;
michael@0 16 import org.mozilla.gecko.util.ThreadUtils;
michael@0 17 import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
michael@0 18
michael@0 19 import android.R;
michael@0 20 import android.content.Context;
michael@0 21 import android.os.Build;
michael@0 22 import android.os.Handler;
michael@0 23 import android.os.Looper;
michael@0 24 import android.os.SystemClock;
michael@0 25 import android.text.Editable;
michael@0 26 import android.text.InputType;
michael@0 27 import android.text.Selection;
michael@0 28 import android.text.SpannableString;
michael@0 29 import android.text.method.KeyListener;
michael@0 30 import android.text.method.TextKeyListener;
michael@0 31 import android.util.DisplayMetrics;
michael@0 32 import android.util.Log;
michael@0 33 import android.view.KeyEvent;
michael@0 34 import android.view.View;
michael@0 35 import android.view.inputmethod.BaseInputConnection;
michael@0 36 import android.view.inputmethod.EditorInfo;
michael@0 37 import android.view.inputmethod.ExtractedText;
michael@0 38 import android.view.inputmethod.ExtractedTextRequest;
michael@0 39 import android.view.inputmethod.InputConnection;
michael@0 40 import android.view.inputmethod.InputMethodManager;
michael@0 41
michael@0 42 class GeckoInputConnection
michael@0 43 extends BaseInputConnection
michael@0 44 implements InputConnectionHandler, GeckoEditableListener {
michael@0 45
michael@0 46 private static final boolean DEBUG = false;
michael@0 47 protected static final String LOGTAG = "GeckoInputConnection";
michael@0 48
michael@0 49 private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
michael@0 50 private static final String CUSTOM_HANDLER_TEST_CLASS =
michael@0 51 "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
michael@0 52
michael@0 53 private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
michael@0 54
michael@0 55 private static Handler sBackgroundHandler;
michael@0 56
michael@0 57 private static class InputThreadUtils {
michael@0 58 // We only want one UI editable around to keep synchronization simple,
michael@0 59 // so we make InputThreadUtils a singleton
michael@0 60 public static final InputThreadUtils sInstance = new InputThreadUtils();
michael@0 61
michael@0 62 private Editable mUiEditable;
michael@0 63 private Object mUiEditableReturn;
michael@0 64 private Exception mUiEditableException;
michael@0 65 private final SynchronousQueue<Runnable> mIcRunnableSync;
michael@0 66 private final Runnable mIcSignalRunnable;
michael@0 67
michael@0 68 private InputThreadUtils() {
michael@0 69 mIcRunnableSync = new SynchronousQueue<Runnable>();
michael@0 70 mIcSignalRunnable = new Runnable() {
michael@0 71 @Override public void run() {
michael@0 72 }
michael@0 73 };
michael@0 74 }
michael@0 75
michael@0 76 private void runOnIcThread(Handler icHandler, final Runnable runnable) {
michael@0 77 if (DEBUG) {
michael@0 78 ThreadUtils.assertOnUiThread();
michael@0 79 Log.d(LOGTAG, "runOnIcThread() on thread " +
michael@0 80 icHandler.getLooper().getThread().getName());
michael@0 81 }
michael@0 82 Runnable runner = new Runnable() {
michael@0 83 @Override public void run() {
michael@0 84 try {
michael@0 85 Runnable queuedRunnable = mIcRunnableSync.take();
michael@0 86 if (DEBUG && queuedRunnable != runnable) {
michael@0 87 throw new IllegalThreadStateException("sync error");
michael@0 88 }
michael@0 89 queuedRunnable.run();
michael@0 90 } catch (InterruptedException e) {
michael@0 91 }
michael@0 92 }
michael@0 93 };
michael@0 94 try {
michael@0 95 // if we are not inside waitForUiThread(), runner will call the runnable
michael@0 96 icHandler.post(runner);
michael@0 97 // runnable will be called by either runner from above or waitForUiThread()
michael@0 98 mIcRunnableSync.put(runnable);
michael@0 99 } catch (InterruptedException e) {
michael@0 100 } finally {
michael@0 101 // if waitForUiThread() already called runnable, runner should not call it again
michael@0 102 icHandler.removeCallbacks(runner);
michael@0 103 }
michael@0 104 }
michael@0 105
michael@0 106 public void endWaitForUiThread() {
michael@0 107 if (DEBUG) {
michael@0 108 ThreadUtils.assertOnUiThread();
michael@0 109 Log.d(LOGTAG, "endWaitForUiThread()");
michael@0 110 }
michael@0 111 try {
michael@0 112 mIcRunnableSync.put(mIcSignalRunnable);
michael@0 113 } catch (InterruptedException e) {
michael@0 114 }
michael@0 115 }
michael@0 116
michael@0 117 public void waitForUiThread(Handler icHandler) {
michael@0 118 if (DEBUG) {
michael@0 119 ThreadUtils.assertOnThread(icHandler.getLooper().getThread(), AssertBehavior.THROW);
michael@0 120 Log.d(LOGTAG, "waitForUiThread() blocking on thread " +
michael@0 121 icHandler.getLooper().getThread().getName());
michael@0 122 }
michael@0 123 try {
michael@0 124 Runnable runnable = null;
michael@0 125 do {
michael@0 126 runnable = mIcRunnableSync.take();
michael@0 127 runnable.run();
michael@0 128 } while (runnable != mIcSignalRunnable);
michael@0 129 } catch (InterruptedException e) {
michael@0 130 }
michael@0 131 }
michael@0 132
michael@0 133 public void runOnIcThread(final Handler uiHandler,
michael@0 134 final GeckoEditableClient client,
michael@0 135 final Runnable runnable) {
michael@0 136 final Handler icHandler = client.getInputConnectionHandler();
michael@0 137 if (icHandler.getLooper() == uiHandler.getLooper()) {
michael@0 138 // IC thread is UI thread; safe to run directly
michael@0 139 runnable.run();
michael@0 140 return;
michael@0 141 }
michael@0 142 runOnIcThread(icHandler, runnable);
michael@0 143 }
michael@0 144
michael@0 145 public void sendEventFromUiThread(final Handler uiHandler,
michael@0 146 final GeckoEditableClient client,
michael@0 147 final GeckoEvent event) {
michael@0 148 runOnIcThread(uiHandler, client, new Runnable() {
michael@0 149 @Override public void run() {
michael@0 150 client.sendEvent(event);
michael@0 151 }
michael@0 152 });
michael@0 153 }
michael@0 154
michael@0 155 public Editable getEditableForUiThread(final Handler uiHandler,
michael@0 156 final GeckoEditableClient client) {
michael@0 157 if (DEBUG) {
michael@0 158 ThreadUtils.assertOnThread(uiHandler.getLooper().getThread(), AssertBehavior.THROW);
michael@0 159 }
michael@0 160 final Handler icHandler = client.getInputConnectionHandler();
michael@0 161 if (icHandler.getLooper() == uiHandler.getLooper()) {
michael@0 162 // IC thread is UI thread; safe to use Editable directly
michael@0 163 return client.getEditable();
michael@0 164 }
michael@0 165 // IC thread is not UI thread; we need to return a proxy Editable in order
michael@0 166 // to safely use the Editable from the UI thread
michael@0 167 if (mUiEditable != null) {
michael@0 168 return mUiEditable;
michael@0 169 }
michael@0 170 final InvocationHandler invokeEditable = new InvocationHandler() {
michael@0 171 @Override public Object invoke(final Object proxy,
michael@0 172 final Method method,
michael@0 173 final Object[] args) throws Throwable {
michael@0 174 if (DEBUG) {
michael@0 175 ThreadUtils.assertOnThread(uiHandler.getLooper().getThread(), AssertBehavior.THROW);
michael@0 176 Log.d(LOGTAG, "UiEditable." + method.getName() + "() blocking");
michael@0 177 }
michael@0 178 synchronized (icHandler) {
michael@0 179 // Now we are on UI thread
michael@0 180 mUiEditableReturn = null;
michael@0 181 mUiEditableException = null;
michael@0 182 // Post a Runnable that calls the real Editable and saves any
michael@0 183 // result/exception. Then wait on the Runnable to finish
michael@0 184 runOnIcThread(icHandler, new Runnable() {
michael@0 185 @Override public void run() {
michael@0 186 synchronized (icHandler) {
michael@0 187 try {
michael@0 188 mUiEditableReturn = method.invoke(
michael@0 189 client.getEditable(), args);
michael@0 190 } catch (Exception e) {
michael@0 191 mUiEditableException = e;
michael@0 192 }
michael@0 193 if (DEBUG) {
michael@0 194 Log.d(LOGTAG, "UiEditable." + method.getName() +
michael@0 195 "() returning");
michael@0 196 }
michael@0 197 icHandler.notify();
michael@0 198 }
michael@0 199 }
michael@0 200 });
michael@0 201 // let InterruptedException propagate
michael@0 202 icHandler.wait();
michael@0 203 if (mUiEditableException != null) {
michael@0 204 throw mUiEditableException;
michael@0 205 }
michael@0 206 return mUiEditableReturn;
michael@0 207 }
michael@0 208 }
michael@0 209 };
michael@0 210 mUiEditable = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(),
michael@0 211 new Class<?>[] { Editable.class }, invokeEditable);
michael@0 212 return mUiEditable;
michael@0 213 }
michael@0 214 }
michael@0 215
michael@0 216 // Managed only by notifyIMEContext; see comments in notifyIMEContext
michael@0 217 private int mIMEState;
michael@0 218 private String mIMETypeHint = "";
michael@0 219 private String mIMEModeHint = "";
michael@0 220 private String mIMEActionHint = "";
michael@0 221
michael@0 222 private String mCurrentInputMethod = "";
michael@0 223
michael@0 224 private final GeckoEditableClient mEditableClient;
michael@0 225 protected int mBatchEditCount;
michael@0 226 private ExtractedTextRequest mUpdateRequest;
michael@0 227 private final ExtractedText mUpdateExtract = new ExtractedText();
michael@0 228 private boolean mBatchSelectionChanged;
michael@0 229 private boolean mBatchTextChanged;
michael@0 230 private long mLastRestartInputTime;
michael@0 231 private final InputConnection mKeyInputConnection;
michael@0 232
michael@0 233 public static GeckoEditableListener create(View targetView,
michael@0 234 GeckoEditableClient editable) {
michael@0 235 if (DEBUG)
michael@0 236 return DebugGeckoInputConnection.create(targetView, editable);
michael@0 237 else
michael@0 238 return new GeckoInputConnection(targetView, editable);
michael@0 239 }
michael@0 240
michael@0 241 protected GeckoInputConnection(View targetView,
michael@0 242 GeckoEditableClient editable) {
michael@0 243 super(targetView, true);
michael@0 244 mEditableClient = editable;
michael@0 245 mIMEState = IME_STATE_DISABLED;
michael@0 246 // InputConnection that sends keys for plugins, which don't have full editors
michael@0 247 mKeyInputConnection = new BaseInputConnection(targetView, false);
michael@0 248 }
michael@0 249
michael@0 250 @Override
michael@0 251 public synchronized boolean beginBatchEdit() {
michael@0 252 mBatchEditCount++;
michael@0 253 mEditableClient.setUpdateGecko(false);
michael@0 254 return true;
michael@0 255 }
michael@0 256
michael@0 257 @Override
michael@0 258 public synchronized boolean endBatchEdit() {
michael@0 259 if (mBatchEditCount > 0) {
michael@0 260 mBatchEditCount--;
michael@0 261 if (mBatchEditCount == 0) {
michael@0 262 if (mBatchTextChanged) {
michael@0 263 notifyTextChange();
michael@0 264 mBatchTextChanged = false;
michael@0 265 }
michael@0 266 if (mBatchSelectionChanged) {
michael@0 267 Editable editable = getEditable();
michael@0 268 notifySelectionChange(Selection.getSelectionStart(editable),
michael@0 269 Selection.getSelectionEnd(editable));
michael@0 270 mBatchSelectionChanged = false;
michael@0 271 }
michael@0 272 mEditableClient.setUpdateGecko(true);
michael@0 273 }
michael@0 274 } else {
michael@0 275 Log.w(LOGTAG, "endBatchEdit() called, but mBatchEditCount == 0?!");
michael@0 276 }
michael@0 277 return true;
michael@0 278 }
michael@0 279
michael@0 280 @Override
michael@0 281 public Editable getEditable() {
michael@0 282 return mEditableClient.getEditable();
michael@0 283 }
michael@0 284
michael@0 285 @Override
michael@0 286 public boolean performContextMenuAction(int id) {
michael@0 287 Editable editable = getEditable();
michael@0 288 if (editable == null) {
michael@0 289 return false;
michael@0 290 }
michael@0 291 int selStart = Selection.getSelectionStart(editable);
michael@0 292 int selEnd = Selection.getSelectionEnd(editable);
michael@0 293
michael@0 294 switch (id) {
michael@0 295 case R.id.selectAll:
michael@0 296 setSelection(0, editable.length());
michael@0 297 break;
michael@0 298 case R.id.cut:
michael@0 299 // If selection is empty, we'll select everything
michael@0 300 if (selStart == selEnd) {
michael@0 301 // Fill the clipboard
michael@0 302 Clipboard.setText(editable);
michael@0 303 editable.clear();
michael@0 304 } else {
michael@0 305 Clipboard.setText(
michael@0 306 editable.toString().substring(
michael@0 307 Math.min(selStart, selEnd),
michael@0 308 Math.max(selStart, selEnd)));
michael@0 309 editable.delete(selStart, selEnd);
michael@0 310 }
michael@0 311 break;
michael@0 312 case R.id.paste:
michael@0 313 commitText(Clipboard.getText(), 1);
michael@0 314 break;
michael@0 315 case R.id.copy:
michael@0 316 // Copy the current selection or the empty string if nothing is selected.
michael@0 317 String copiedText = selStart == selEnd ? "" :
michael@0 318 editable.toString().substring(
michael@0 319 Math.min(selStart, selEnd),
michael@0 320 Math.max(selStart, selEnd));
michael@0 321 Clipboard.setText(copiedText);
michael@0 322 break;
michael@0 323 }
michael@0 324 return true;
michael@0 325 }
michael@0 326
michael@0 327 @Override
michael@0 328 public ExtractedText getExtractedText(ExtractedTextRequest req, int flags) {
michael@0 329 if (req == null)
michael@0 330 return null;
michael@0 331
michael@0 332 if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0)
michael@0 333 mUpdateRequest = req;
michael@0 334
michael@0 335 Editable editable = getEditable();
michael@0 336 if (editable == null) {
michael@0 337 return null;
michael@0 338 }
michael@0 339 int selStart = Selection.getSelectionStart(editable);
michael@0 340 int selEnd = Selection.getSelectionEnd(editable);
michael@0 341
michael@0 342 ExtractedText extract = new ExtractedText();
michael@0 343 extract.flags = 0;
michael@0 344 extract.partialStartOffset = -1;
michael@0 345 extract.partialEndOffset = -1;
michael@0 346 extract.selectionStart = selStart;
michael@0 347 extract.selectionEnd = selEnd;
michael@0 348 extract.startOffset = 0;
michael@0 349 if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
michael@0 350 extract.text = new SpannableString(editable);
michael@0 351 } else {
michael@0 352 extract.text = editable.toString();
michael@0 353 }
michael@0 354 return extract;
michael@0 355 }
michael@0 356
michael@0 357 private static View getView() {
michael@0 358 return GeckoAppShell.getLayerView();
michael@0 359 }
michael@0 360
michael@0 361 private static InputMethodManager getInputMethodManager() {
michael@0 362 View view = getView();
michael@0 363 if (view == null) {
michael@0 364 return null;
michael@0 365 }
michael@0 366 Context context = view.getContext();
michael@0 367 return InputMethods.getInputMethodManager(context);
michael@0 368 }
michael@0 369
michael@0 370 private static void showSoftInput() {
michael@0 371 final InputMethodManager imm = getInputMethodManager();
michael@0 372 if (imm != null) {
michael@0 373 final View v = getView();
michael@0 374 imm.showSoftInput(v, 0);
michael@0 375 }
michael@0 376 }
michael@0 377
michael@0 378 private static void hideSoftInput() {
michael@0 379 final InputMethodManager imm = getInputMethodManager();
michael@0 380 if (imm != null) {
michael@0 381 final View v = getView();
michael@0 382 imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
michael@0 383 }
michael@0 384 }
michael@0 385
michael@0 386 private void tryRestartInput() {
michael@0 387 // Coalesce restartInput calls because InputMethodManager.restartInput()
michael@0 388 // is expensive and successive calls to it can lock up the keyboard
michael@0 389 if (SystemClock.uptimeMillis() < mLastRestartInputTime + 200) {
michael@0 390 return;
michael@0 391 }
michael@0 392 restartInput();
michael@0 393 }
michael@0 394
michael@0 395 private void restartInput() {
michael@0 396
michael@0 397 mLastRestartInputTime = SystemClock.uptimeMillis();
michael@0 398
michael@0 399 final InputMethodManager imm = getInputMethodManager();
michael@0 400 if (imm == null) {
michael@0 401 return;
michael@0 402 }
michael@0 403 final View v = getView();
michael@0 404 // InputMethodManager has internal logic to detect if we are restarting input
michael@0 405 // in an already focused View, which is the case here because all content text
michael@0 406 // fields are inside one LayerView. When this happens, InputMethodManager will
michael@0 407 // tell the input method to soft reset instead of hard reset. Stock latin IME
michael@0 408 // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
michael@0 409 // composition. The following workaround tricks the IME into clearing the
michael@0 410 // composition when soft resetting.
michael@0 411 if (InputMethods.needsSoftResetWorkaround(mCurrentInputMethod)) {
michael@0 412 // Fake a selection change, because the IME clears the composition when
michael@0 413 // the selection changes, even if soft-resetting. Offsets here must be
michael@0 414 // different from the previous selection offsets, and -1 seems to be a
michael@0 415 // reasonable, deterministic value
michael@0 416 notifySelectionChange(-1, -1);
michael@0 417 }
michael@0 418 imm.restartInput(v);
michael@0 419 }
michael@0 420
michael@0 421 private void resetInputConnection() {
michael@0 422 if (mBatchEditCount != 0) {
michael@0 423 Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
michael@0 424 mBatchEditCount = 0;
michael@0 425 }
michael@0 426 mBatchSelectionChanged = false;
michael@0 427 mBatchTextChanged = false;
michael@0 428
michael@0 429 // Do not reset mIMEState here; see comments in notifyIMEContext
michael@0 430 }
michael@0 431
michael@0 432 @Override
michael@0 433 public void onTextChange(String text, int start, int oldEnd, int newEnd) {
michael@0 434
michael@0 435 if (mUpdateRequest == null) {
michael@0 436 // Android always expects selection updates when not in extracted mode;
michael@0 437 // in extracted mode, the selection is reported through updateExtractedText
michael@0 438 final Editable editable = getEditable();
michael@0 439 if (editable != null) {
michael@0 440 onSelectionChange(Selection.getSelectionStart(editable),
michael@0 441 Selection.getSelectionEnd(editable));
michael@0 442 }
michael@0 443 return;
michael@0 444 }
michael@0 445
michael@0 446 if (mBatchEditCount > 0) {
michael@0 447 // Delay notification until after the batch edit
michael@0 448 mBatchTextChanged = true;
michael@0 449 return;
michael@0 450 }
michael@0 451 notifyTextChange();
michael@0 452 }
michael@0 453
michael@0 454 private void notifyTextChange() {
michael@0 455
michael@0 456 final InputMethodManager imm = getInputMethodManager();
michael@0 457 final View v = getView();
michael@0 458 final Editable editable = getEditable();
michael@0 459 if (imm == null || v == null || editable == null) {
michael@0 460 return;
michael@0 461 }
michael@0 462 mUpdateExtract.flags = 0;
michael@0 463 // Update the entire Editable range
michael@0 464 mUpdateExtract.partialStartOffset = -1;
michael@0 465 mUpdateExtract.partialEndOffset = -1;
michael@0 466 mUpdateExtract.selectionStart =
michael@0 467 Selection.getSelectionStart(editable);
michael@0 468 mUpdateExtract.selectionEnd =
michael@0 469 Selection.getSelectionEnd(editable);
michael@0 470 mUpdateExtract.startOffset = 0;
michael@0 471 if ((mUpdateRequest.flags & GET_TEXT_WITH_STYLES) != 0) {
michael@0 472 mUpdateExtract.text = new SpannableString(editable);
michael@0 473 } else {
michael@0 474 mUpdateExtract.text = editable.toString();
michael@0 475 }
michael@0 476 imm.updateExtractedText(v, mUpdateRequest.token,
michael@0 477 mUpdateExtract);
michael@0 478 }
michael@0 479
michael@0 480 @Override
michael@0 481 public void onSelectionChange(int start, int end) {
michael@0 482
michael@0 483 if (mBatchEditCount > 0) {
michael@0 484 // Delay notification until after the batch edit
michael@0 485 mBatchSelectionChanged = true;
michael@0 486 return;
michael@0 487 }
michael@0 488 notifySelectionChange(start, end);
michael@0 489 }
michael@0 490
michael@0 491 private void notifySelectionChange(int start, int end) {
michael@0 492
michael@0 493 final InputMethodManager imm = getInputMethodManager();
michael@0 494 final View v = getView();
michael@0 495 final Editable editable = getEditable();
michael@0 496 if (imm == null || v == null || editable == null) {
michael@0 497 return;
michael@0 498 }
michael@0 499 imm.updateSelection(v, start, end, getComposingSpanStart(editable),
michael@0 500 getComposingSpanEnd(editable));
michael@0 501 }
michael@0 502
michael@0 503 private static synchronized Handler getBackgroundHandler() {
michael@0 504 if (sBackgroundHandler != null) {
michael@0 505 return sBackgroundHandler;
michael@0 506 }
michael@0 507 // Don't use GeckoBackgroundThread because Gecko thread may block waiting on
michael@0 508 // GeckoBackgroundThread. If we were to use GeckoBackgroundThread, due to IME,
michael@0 509 // GeckoBackgroundThread may end up also block waiting on Gecko thread and a
michael@0 510 // deadlock occurs
michael@0 511 Thread backgroundThread = new Thread(new Runnable() {
michael@0 512 @Override
michael@0 513 public void run() {
michael@0 514 Looper.prepare();
michael@0 515 synchronized (GeckoInputConnection.class) {
michael@0 516 sBackgroundHandler = new Handler();
michael@0 517 GeckoInputConnection.class.notify();
michael@0 518 }
michael@0 519 Looper.loop();
michael@0 520 sBackgroundHandler = null;
michael@0 521 }
michael@0 522 }, LOGTAG);
michael@0 523 backgroundThread.setDaemon(true);
michael@0 524 backgroundThread.start();
michael@0 525 while (sBackgroundHandler == null) {
michael@0 526 try {
michael@0 527 // wait for new thread to set sBackgroundHandler
michael@0 528 GeckoInputConnection.class.wait();
michael@0 529 } catch (InterruptedException e) {
michael@0 530 }
michael@0 531 }
michael@0 532 return sBackgroundHandler;
michael@0 533 }
michael@0 534
michael@0 535 private boolean canReturnCustomHandler() {
michael@0 536 if (mIMEState == IME_STATE_DISABLED) {
michael@0 537 return false;
michael@0 538 }
michael@0 539 for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
michael@0 540 // We only return our custom Handler to InputMethodManager's InputConnection
michael@0 541 // proxy. For all other purposes, we return the regular Handler.
michael@0 542 // InputMethodManager retrieves the Handler for its InputConnection proxy
michael@0 543 // inside its method startInputInner(), so we check for that here. This is
michael@0 544 // valid from Android 2.2 to at least Android 4.2. If this situation ever
michael@0 545 // changes, we gracefully fall back to using the regular Handler.
michael@0 546 if ("startInputInner".equals(frame.getMethodName()) &&
michael@0 547 "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
michael@0 548 // only return our own Handler to InputMethodManager
michael@0 549 return true;
michael@0 550 }
michael@0 551 if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) &&
michael@0 552 CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
michael@0 553 // InputConnection tests should also run on the custom handler
michael@0 554 return true;
michael@0 555 }
michael@0 556 }
michael@0 557 return false;
michael@0 558 }
michael@0 559
michael@0 560 @Override
michael@0 561 public Handler getHandler(Handler defHandler) {
michael@0 562 if (!canReturnCustomHandler()) {
michael@0 563 return defHandler;
michael@0 564 }
michael@0 565 // getBackgroundHandler() is synchronized and requires locking,
michael@0 566 // but if we already have our handler, we don't have to lock
michael@0 567 final Handler newHandler = sBackgroundHandler != null
michael@0 568 ? sBackgroundHandler
michael@0 569 : getBackgroundHandler();
michael@0 570 if (mEditableClient.setInputConnectionHandler(newHandler)) {
michael@0 571 return newHandler;
michael@0 572 }
michael@0 573 // Setting new IC handler failed; return old IC handler
michael@0 574 return mEditableClient.getInputConnectionHandler();
michael@0 575 }
michael@0 576
michael@0 577 @Override
michael@0 578 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
michael@0 579 if (mIMEState == IME_STATE_DISABLED) {
michael@0 580 return null;
michael@0 581 }
michael@0 582
michael@0 583 outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
michael@0 584 outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
michael@0 585 outAttrs.actionLabel = null;
michael@0 586
michael@0 587 if (mIMEState == IME_STATE_PASSWORD ||
michael@0 588 "password".equalsIgnoreCase(mIMETypeHint))
michael@0 589 outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
michael@0 590 else if (mIMEState == IME_STATE_PLUGIN)
michael@0 591 outAttrs.inputType = InputType.TYPE_NULL; // "send key events" mode
michael@0 592 else if (mIMETypeHint.equalsIgnoreCase("url"))
michael@0 593 outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
michael@0 594 else if (mIMETypeHint.equalsIgnoreCase("email"))
michael@0 595 outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
michael@0 596 else if (mIMETypeHint.equalsIgnoreCase("search"))
michael@0 597 outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
michael@0 598 else if (mIMETypeHint.equalsIgnoreCase("tel"))
michael@0 599 outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
michael@0 600 else if (mIMETypeHint.equalsIgnoreCase("number") ||
michael@0 601 mIMETypeHint.equalsIgnoreCase("range"))
michael@0 602 outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
michael@0 603 | InputType.TYPE_NUMBER_FLAG_SIGNED
michael@0 604 | InputType.TYPE_NUMBER_FLAG_DECIMAL;
michael@0 605 else if (mIMETypeHint.equalsIgnoreCase("week") ||
michael@0 606 mIMETypeHint.equalsIgnoreCase("month"))
michael@0 607 outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
michael@0 608 | InputType.TYPE_DATETIME_VARIATION_DATE;
michael@0 609 else if (mIMEModeHint.equalsIgnoreCase("numeric"))
michael@0 610 outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
michael@0 611 InputType.TYPE_NUMBER_FLAG_SIGNED |
michael@0 612 InputType.TYPE_NUMBER_FLAG_DECIMAL;
michael@0 613 else if (mIMEModeHint.equalsIgnoreCase("digit"))
michael@0 614 outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
michael@0 615 else {
michael@0 616 // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
michael@0 617 outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
michael@0 618 InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
michael@0 619 if (mIMETypeHint.equalsIgnoreCase("textarea") ||
michael@0 620 mIMETypeHint.length() == 0) {
michael@0 621 // empty mIMETypeHint indicates contentEditable/designMode documents
michael@0 622 outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
michael@0 623 }
michael@0 624 if (mIMEModeHint.equalsIgnoreCase("uppercase"))
michael@0 625 outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
michael@0 626 else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
michael@0 627 outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
michael@0 628 else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
michael@0 629 outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
michael@0 630 // auto-capitalized mode is the default
michael@0 631 }
michael@0 632
michael@0 633 if (mIMEActionHint.equalsIgnoreCase("go"))
michael@0 634 outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
michael@0 635 else if (mIMEActionHint.equalsIgnoreCase("done"))
michael@0 636 outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
michael@0 637 else if (mIMEActionHint.equalsIgnoreCase("next"))
michael@0 638 outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
michael@0 639 else if (mIMEActionHint.equalsIgnoreCase("search"))
michael@0 640 outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
michael@0 641 else if (mIMEActionHint.equalsIgnoreCase("send"))
michael@0 642 outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
michael@0 643 else if (mIMEActionHint.length() > 0) {
michael@0 644 if (DEBUG)
michael@0 645 Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
michael@0 646 outAttrs.actionLabel = mIMEActionHint;
michael@0 647 }
michael@0 648
michael@0 649 Context context = GeckoAppShell.getContext();
michael@0 650 DisplayMetrics metrics = context.getResources().getDisplayMetrics();
michael@0 651 if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
michael@0 652 // prevent showing full-screen keyboard only when the screen is tall enough
michael@0 653 // to show some reasonable amount of the page (see bug 752709)
michael@0 654 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI
michael@0 655 | EditorInfo.IME_FLAG_NO_FULLSCREEN;
michael@0 656 }
michael@0 657
michael@0 658 if (DEBUG) {
michael@0 659 Log.d(LOGTAG, "mapped IME states to: inputType = " +
michael@0 660 Integer.toHexString(outAttrs.inputType) + ", imeOptions = " +
michael@0 661 Integer.toHexString(outAttrs.imeOptions));
michael@0 662 }
michael@0 663
michael@0 664 String prevInputMethod = mCurrentInputMethod;
michael@0 665 mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
michael@0 666 if (DEBUG) {
michael@0 667 Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
michael@0 668 }
michael@0 669
michael@0 670 // If the user has changed IMEs, then notify input method observers.
michael@0 671 if (!mCurrentInputMethod.equals(prevInputMethod) && GeckoAppShell.getGeckoInterface() != null) {
michael@0 672 FormAssistPopup popup = GeckoAppShell.getGeckoInterface().getFormAssistPopup();
michael@0 673 if (popup != null) {
michael@0 674 popup.onInputMethodChanged(mCurrentInputMethod);
michael@0 675 }
michael@0 676 }
michael@0 677
michael@0 678 if (mIMEState == IME_STATE_PLUGIN) {
michael@0 679 // Since we are using a temporary string as the editable, the selection is at 0
michael@0 680 outAttrs.initialSelStart = 0;
michael@0 681 outAttrs.initialSelEnd = 0;
michael@0 682 return mKeyInputConnection;
michael@0 683 }
michael@0 684 Editable editable = getEditable();
michael@0 685 outAttrs.initialSelStart = Selection.getSelectionStart(editable);
michael@0 686 outAttrs.initialSelEnd = Selection.getSelectionEnd(editable);
michael@0 687 return this;
michael@0 688 }
michael@0 689
michael@0 690 private boolean replaceComposingSpanWithSelection() {
michael@0 691 final Editable content = getEditable();
michael@0 692 if (content == null) {
michael@0 693 return false;
michael@0 694 }
michael@0 695 int a = getComposingSpanStart(content),
michael@0 696 b = getComposingSpanEnd(content);
michael@0 697 if (a != -1 && b != -1) {
michael@0 698 if (DEBUG) {
michael@0 699 Log.d(LOGTAG, "removing composition at " + a + "-" + b);
michael@0 700 }
michael@0 701 removeComposingSpans(content);
michael@0 702 Selection.setSelection(content, a, b);
michael@0 703 }
michael@0 704 return true;
michael@0 705 }
michael@0 706
michael@0 707 @Override
michael@0 708 public boolean commitText(CharSequence text, int newCursorPosition) {
michael@0 709 if (InputMethods.shouldCommitCharAsKey(mCurrentInputMethod) &&
michael@0 710 text.length() == 1 && newCursorPosition > 0) {
michael@0 711 if (DEBUG) {
michael@0 712 Log.d(LOGTAG, "committing \"" + text + "\" as key");
michael@0 713 }
michael@0 714 // mKeyInputConnection is a BaseInputConnection that commits text as keys;
michael@0 715 // but we first need to replace any composing span with a selection,
michael@0 716 // so that the new key events will generate characters to replace
michael@0 717 // text from the old composing span
michael@0 718 return replaceComposingSpanWithSelection() &&
michael@0 719 mKeyInputConnection.commitText(text, newCursorPosition);
michael@0 720 }
michael@0 721 return super.commitText(text, newCursorPosition);
michael@0 722 }
michael@0 723
michael@0 724 @Override
michael@0 725 public boolean setSelection(int start, int end) {
michael@0 726 if (start < 0 || end < 0) {
michael@0 727 // Some keyboards (e.g. Samsung) can call setSelection with
michael@0 728 // negative offsets. In that case we ignore the call, similar to how
michael@0 729 // BaseInputConnection.setSelection ignores offsets that go past the length.
michael@0 730 return true;
michael@0 731 }
michael@0 732 return super.setSelection(start, end);
michael@0 733 }
michael@0 734
michael@0 735 @Override
michael@0 736 public boolean sendKeyEvent(KeyEvent event) {
michael@0 737 // BaseInputConnection.sendKeyEvent() dispatches the key event to the main thread.
michael@0 738 // In order to ensure events are processed in the proper order, we must block the
michael@0 739 // IC thread until the main thread finishes processing the key event
michael@0 740 super.sendKeyEvent(event);
michael@0 741 final View v = getView();
michael@0 742 if (v == null) {
michael@0 743 return false;
michael@0 744 }
michael@0 745 final Handler icHandler = mEditableClient.getInputConnectionHandler();
michael@0 746 final Handler mainHandler = v.getRootView().getHandler();
michael@0 747 if (icHandler.getLooper() != mainHandler.getLooper()) {
michael@0 748 // We are on separate IC thread but the event is queued on the main thread;
michael@0 749 // wait on IC thread until the main thread processes our posted Runnable. At
michael@0 750 // that point the key event has already been processed.
michael@0 751 mainHandler.post(new Runnable() {
michael@0 752 @Override public void run() {
michael@0 753 InputThreadUtils.sInstance.endWaitForUiThread();
michael@0 754 }
michael@0 755 });
michael@0 756 InputThreadUtils.sInstance.waitForUiThread(icHandler);
michael@0 757 }
michael@0 758 return false; // seems to always return false
michael@0 759 }
michael@0 760
michael@0 761 @Override
michael@0 762 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
michael@0 763 return false;
michael@0 764 }
michael@0 765
michael@0 766 private boolean shouldProcessKey(int keyCode, KeyEvent event) {
michael@0 767 switch (keyCode) {
michael@0 768 case KeyEvent.KEYCODE_MENU:
michael@0 769 case KeyEvent.KEYCODE_BACK:
michael@0 770 case KeyEvent.KEYCODE_VOLUME_UP:
michael@0 771 case KeyEvent.KEYCODE_VOLUME_DOWN:
michael@0 772 case KeyEvent.KEYCODE_SEARCH:
michael@0 773 return false;
michael@0 774 }
michael@0 775 return true;
michael@0 776 }
michael@0 777
michael@0 778 private boolean shouldSkipKeyListener(int keyCode, KeyEvent event) {
michael@0 779 if (mIMEState == IME_STATE_DISABLED ||
michael@0 780 mIMEState == IME_STATE_PLUGIN) {
michael@0 781 return true;
michael@0 782 }
michael@0 783 // Preserve enter and tab keys for the browser
michael@0 784 if (keyCode == KeyEvent.KEYCODE_ENTER ||
michael@0 785 keyCode == KeyEvent.KEYCODE_TAB) {
michael@0 786 return true;
michael@0 787 }
michael@0 788 // BaseKeyListener returns false even if it handled these keys for us,
michael@0 789 // so we skip the key listener entirely and handle these ourselves
michael@0 790 if (keyCode == KeyEvent.KEYCODE_DEL ||
michael@0 791 keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
michael@0 792 return true;
michael@0 793 }
michael@0 794 return false;
michael@0 795 }
michael@0 796
michael@0 797 private KeyEvent translateKey(int keyCode, KeyEvent event) {
michael@0 798 switch (keyCode) {
michael@0 799 case KeyEvent.KEYCODE_ENTER:
michael@0 800 if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 &&
michael@0 801 mIMEActionHint.equalsIgnoreCase("next")) {
michael@0 802 return new KeyEvent(event.getAction(), KeyEvent.KEYCODE_TAB);
michael@0 803 }
michael@0 804 break;
michael@0 805 }
michael@0 806 return event;
michael@0 807 }
michael@0 808
michael@0 809 private boolean processKey(int keyCode, KeyEvent event, boolean down) {
michael@0 810 if (GamepadUtils.isSonyXperiaGamepadKeyEvent(event)) {
michael@0 811 event = GamepadUtils.translateSonyXperiaGamepadKeys(keyCode, event);
michael@0 812 keyCode = event.getKeyCode();
michael@0 813 }
michael@0 814
michael@0 815 if (keyCode > KeyEvent.getMaxKeyCode() ||
michael@0 816 !shouldProcessKey(keyCode, event)) {
michael@0 817 return false;
michael@0 818 }
michael@0 819 event = translateKey(keyCode, event);
michael@0 820 keyCode = event.getKeyCode();
michael@0 821
michael@0 822 View view = getView();
michael@0 823 if (view == null) {
michael@0 824 InputThreadUtils.sInstance.sendEventFromUiThread(ThreadUtils.getUiHandler(),
michael@0 825 mEditableClient, GeckoEvent.createKeyEvent(event, 0));
michael@0 826 return true;
michael@0 827 }
michael@0 828
michael@0 829 // KeyListener returns true if it handled the event for us. KeyListener is only
michael@0 830 // safe to use on the UI thread; therefore we need to pass a proxy Editable to it
michael@0 831 KeyListener keyListener = TextKeyListener.getInstance();
michael@0 832 Handler uiHandler = view.getRootView().getHandler();
michael@0 833 Editable uiEditable = InputThreadUtils.sInstance.
michael@0 834 getEditableForUiThread(uiHandler, mEditableClient);
michael@0 835 boolean skip = shouldSkipKeyListener(keyCode, event);
michael@0 836 if (down) {
michael@0 837 mEditableClient.setSuppressKeyUp(true);
michael@0 838 }
michael@0 839 if (skip ||
michael@0 840 (down && !keyListener.onKeyDown(view, uiEditable, keyCode, event)) ||
michael@0 841 (!down && !keyListener.onKeyUp(view, uiEditable, keyCode, event))) {
michael@0 842 InputThreadUtils.sInstance.sendEventFromUiThread(uiHandler, mEditableClient,
michael@0 843 GeckoEvent.createKeyEvent(event, TextKeyListener.getMetaState(uiEditable)));
michael@0 844 if (skip && down) {
michael@0 845 // Usually, the down key listener call above adjusts meta states for us.
michael@0 846 // However, if we skip that call above, we have to manually adjust meta
michael@0 847 // states so the meta states remain consistent
michael@0 848 TextKeyListener.adjustMetaAfterKeypress(uiEditable);
michael@0 849 }
michael@0 850 }
michael@0 851 if (down) {
michael@0 852 mEditableClient.setSuppressKeyUp(false);
michael@0 853 }
michael@0 854 return true;
michael@0 855 }
michael@0 856
michael@0 857 @Override
michael@0 858 public boolean onKeyDown(int keyCode, KeyEvent event) {
michael@0 859 return processKey(keyCode, event, true);
michael@0 860 }
michael@0 861
michael@0 862 @Override
michael@0 863 public boolean onKeyUp(int keyCode, KeyEvent event) {
michael@0 864 return processKey(keyCode, event, false);
michael@0 865 }
michael@0 866
michael@0 867 @Override
michael@0 868 public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
michael@0 869 if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
michael@0 870 // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
michael@0 871 View view = getView();
michael@0 872 if (view != null) {
michael@0 873 InputThreadUtils.sInstance.runOnIcThread(
michael@0 874 view.getRootView().getHandler(), mEditableClient,
michael@0 875 new Runnable() {
michael@0 876 @Override public void run() {
michael@0 877 // Don't call GeckoInputConnection.commitText because it can
michael@0 878 // post a key event back to onKeyMultiple, causing a loop
michael@0 879 GeckoInputConnection.super.commitText(event.getCharacters(), 1);
michael@0 880 }
michael@0 881 });
michael@0 882 }
michael@0 883 return true;
michael@0 884 }
michael@0 885 while ((repeatCount--) != 0) {
michael@0 886 if (!processKey(keyCode, event, true) ||
michael@0 887 !processKey(keyCode, event, false)) {
michael@0 888 return false;
michael@0 889 }
michael@0 890 }
michael@0 891 return true;
michael@0 892 }
michael@0 893
michael@0 894 @Override
michael@0 895 public boolean onKeyLongPress(int keyCode, KeyEvent event) {
michael@0 896 View v = getView();
michael@0 897 switch (keyCode) {
michael@0 898 case KeyEvent.KEYCODE_MENU:
michael@0 899 InputMethodManager imm = getInputMethodManager();
michael@0 900 imm.toggleSoftInputFromWindow(v.getWindowToken(),
michael@0 901 InputMethodManager.SHOW_FORCED, 0);
michael@0 902 return true;
michael@0 903 default:
michael@0 904 break;
michael@0 905 }
michael@0 906 return false;
michael@0 907 }
michael@0 908
michael@0 909 @Override
michael@0 910 public boolean isIMEEnabled() {
michael@0 911 // make sure this picks up PASSWORD and PLUGIN states as well
michael@0 912 return mIMEState != IME_STATE_DISABLED;
michael@0 913 }
michael@0 914
michael@0 915 @Override
michael@0 916 public void notifyIME(int type) {
michael@0 917 switch (type) {
michael@0 918
michael@0 919 case NOTIFY_IME_TO_CANCEL_COMPOSITION:
michael@0 920 // Set composition to empty and end composition
michael@0 921 setComposingText("", 0);
michael@0 922 // Fall through
michael@0 923
michael@0 924 case NOTIFY_IME_TO_COMMIT_COMPOSITION:
michael@0 925 // Commit and end composition
michael@0 926 finishComposingText();
michael@0 927 tryRestartInput();
michael@0 928 break;
michael@0 929
michael@0 930 case NOTIFY_IME_OF_FOCUS:
michael@0 931 case NOTIFY_IME_OF_BLUR:
michael@0 932 // Showing/hiding vkb is done in notifyIMEContext
michael@0 933 resetInputConnection();
michael@0 934 break;
michael@0 935
michael@0 936 case NOTIFY_IME_OPEN_VKB:
michael@0 937 showSoftInput();
michael@0 938 break;
michael@0 939
michael@0 940 default:
michael@0 941 if (DEBUG) {
michael@0 942 throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
michael@0 943 }
michael@0 944 break;
michael@0 945 }
michael@0 946 }
michael@0 947
michael@0 948 @Override
michael@0 949 public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint) {
michael@0 950 // For some input type we will use a widget to display the ui, for those we must not
michael@0 951 // display the ime. We can display a widget for date and time types and, if the sdk version
michael@0 952 // is 11 or greater, for datetime/month/week as well.
michael@0 953 if (typeHint != null &&
michael@0 954 (typeHint.equalsIgnoreCase("date") ||
michael@0 955 typeHint.equalsIgnoreCase("time") ||
michael@0 956 (Build.VERSION.SDK_INT >= 11 && (typeHint.equalsIgnoreCase("datetime") ||
michael@0 957 typeHint.equalsIgnoreCase("month") ||
michael@0 958 typeHint.equalsIgnoreCase("week") ||
michael@0 959 typeHint.equalsIgnoreCase("datetime-local"))))) {
michael@0 960 state = IME_STATE_DISABLED;
michael@0 961 }
michael@0 962
michael@0 963 // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
michael@0 964 // and not reset anywhere else. Usually, notifyIMEContext is called right after a
michael@0 965 // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
michael@0 966 // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
michael@0 967 // independent of focus change; that is, a focus change may not be accompanied by
michael@0 968 // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
michael@0 969 // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
michael@0 970 /* When IME is 'disabled', IME processing is disabled.
michael@0 971 In addition, the IME UI is hidden */
michael@0 972 mIMEState = state;
michael@0 973 mIMETypeHint = (typeHint == null) ? "" : typeHint;
michael@0 974 mIMEModeHint = (modeHint == null) ? "" : modeHint;
michael@0 975 mIMEActionHint = (actionHint == null) ? "" : actionHint;
michael@0 976
michael@0 977 // These fields are reset here and will be updated when restartInput is called below
michael@0 978 mUpdateRequest = null;
michael@0 979 mCurrentInputMethod = "";
michael@0 980
michael@0 981 View v = getView();
michael@0 982 if (v == null || !v.hasFocus()) {
michael@0 983 // When using Find In Page, we can still receive notifyIMEContext calls due to the
michael@0 984 // selection changing when highlighting. However in this case we don't want to reset/
michael@0 985 // show/hide the keyboard because the find box has the focus and is taking input from
michael@0 986 // the keyboard.
michael@0 987 return;
michael@0 988 }
michael@0 989 restartInput();
michael@0 990 if (mIMEState == IME_STATE_DISABLED) {
michael@0 991 hideSoftInput();
michael@0 992 } else {
michael@0 993 showSoftInput();
michael@0 994 }
michael@0 995 }
michael@0 996 }
michael@0 997
michael@0 998 final class DebugGeckoInputConnection
michael@0 999 extends GeckoInputConnection
michael@0 1000 implements InvocationHandler {
michael@0 1001
michael@0 1002 private InputConnection mProxy;
michael@0 1003 private StringBuilder mCallLevel;
michael@0 1004
michael@0 1005 private DebugGeckoInputConnection(View targetView,
michael@0 1006 GeckoEditableClient editable) {
michael@0 1007 super(targetView, editable);
michael@0 1008 mCallLevel = new StringBuilder();
michael@0 1009 }
michael@0 1010
michael@0 1011 public static GeckoEditableListener create(View targetView,
michael@0 1012 GeckoEditableClient editable) {
michael@0 1013 final Class[] PROXY_INTERFACES = { InputConnection.class,
michael@0 1014 InputConnectionHandler.class,
michael@0 1015 GeckoEditableListener.class };
michael@0 1016 DebugGeckoInputConnection dgic =
michael@0 1017 new DebugGeckoInputConnection(targetView, editable);
michael@0 1018 dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
michael@0 1019 GeckoInputConnection.class.getClassLoader(),
michael@0 1020 PROXY_INTERFACES, dgic);
michael@0 1021 return (GeckoEditableListener)dgic.mProxy;
michael@0 1022 }
michael@0 1023
michael@0 1024 @Override
michael@0 1025 public Object invoke(Object proxy, Method method, Object[] args)
michael@0 1026 throws Throwable {
michael@0 1027
michael@0 1028 StringBuilder log = new StringBuilder(mCallLevel);
michael@0 1029 log.append("> ").append(method.getName()).append("(");
michael@0 1030 for (Object arg : args) {
michael@0 1031 // translate argument values to constant names
michael@0 1032 if ("notifyIME".equals(method.getName()) && arg == args[0]) {
michael@0 1033 log.append(GeckoEditable.getConstantName(
michael@0 1034 GeckoEditableListener.class, "NOTIFY_IME_", arg));
michael@0 1035 } else if ("notifyIMEContext".equals(method.getName()) && arg == args[0]) {
michael@0 1036 log.append(GeckoEditable.getConstantName(
michael@0 1037 GeckoEditableListener.class, "IME_STATE_", arg));
michael@0 1038 } else {
michael@0 1039 GeckoEditable.debugAppend(log, arg);
michael@0 1040 }
michael@0 1041 log.append(", ");
michael@0 1042 }
michael@0 1043 if (args.length > 0) {
michael@0 1044 log.setLength(log.length() - 2);
michael@0 1045 }
michael@0 1046 log.append(")");
michael@0 1047 Log.d(LOGTAG, log.toString());
michael@0 1048
michael@0 1049 mCallLevel.append(' ');
michael@0 1050 Object ret = method.invoke(this, args);
michael@0 1051 if (ret == this) {
michael@0 1052 ret = mProxy;
michael@0 1053 }
michael@0 1054 mCallLevel.setLength(Math.max(0, mCallLevel.length() - 1));
michael@0 1055
michael@0 1056 log.setLength(mCallLevel.length());
michael@0 1057 log.append("< ").append(method.getName());
michael@0 1058 if (!method.getReturnType().equals(Void.TYPE)) {
michael@0 1059 GeckoEditable.debugAppend(log.append(": "), ret);
michael@0 1060 }
michael@0 1061 Log.d(LOGTAG, log.toString());
michael@0 1062 return ret;
michael@0 1063 }
michael@0 1064 }

mercurial