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