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.

     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();
  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;
  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);
  1041             log.append(", ");
  1043         if (args.length > 0) {
  1044             log.setLength(log.length() - 2);
  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;
  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);
  1061         Log.d(LOGTAG, log.toString());
  1062         return ret;

mercurial