Bug 1464096 - 2. Move restart/show/hide input functionality to GeckoEditable; r?esawin draft
authorJim Chen <nchen@mozilla.com>
Tue, 05 Jun 2018 17:49:01 -0400
changeset 804393 867965bab9fa4ddc423dee7e01cc034105456c80
parent 804392 687fcdddf0c634b1a16dcb174a9808e8fb6ce334
child 804394 674f0c9115b99c7b6bdccf2c468ce067ae004ded
push id112368
push userbmo:nchen@mozilla.com
push dateTue, 05 Jun 2018 21:49:45 +0000
reviewersesawin
bugs1464096
milestone62.0a1
Bug 1464096 - 2. Move restart/show/hide input functionality to GeckoEditable; r?esawin Right now all the code to restart/show/hide the input method is in GeckoInputConnection. However, that means the corresponding SessionTextInput.Delegate methods are not called when GeckoInputConnection is not available, typically due to us not having a View. This patch moves all that code to GeckoEditable or SessionTextInput, so that those delegate methods are always called even if we don't have a View / GeckoInputConnection. MozReview-Commit-ID: 2Ws2RvOXYlZ
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoEditable.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoEditable.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoEditable.java
@@ -6,48 +6,52 @@
 package org.mozilla.geckoview;
 
 import java.lang.reflect.Array;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.mozilla.gecko.GeckoEditableChild;
 import org.mozilla.gecko.IGeckoEditableChild;
 import org.mozilla.gecko.IGeckoEditableParent;
+import org.mozilla.gecko.InputMethods;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
 
 import android.graphics.RectF;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.Editable;
 import android.text.InputFilter;
+import android.text.InputType;
 import android.text.Selection;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextPaint;
 import android.text.TextUtils;
 import android.text.method.KeyListener;
 import android.text.method.TextKeyListener;
 import android.text.style.CharacterStyle;
 import android.util.Log;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.inputmethod.EditorInfo;
 
 /**
  * GeckoEditable implements only some functions of Editable
  * The field mText contains the actual underlying
  * SpannableStringBuilder/Editable that contains our text.
  */
 /* package */ final class GeckoEditable
     extends IGeckoEditableParent.Stub
@@ -56,16 +60,17 @@ import android.view.View;
                SessionTextInput.EditableClient {
 
     private static final boolean DEBUG = false;
     private static final String LOGTAG = "GeckoEditable";
 
     // Filters to implement Editable's filtering functionality
     private InputFilter[] mFilters;
 
+    /* package */ final GeckoSession mSession;
     private final AsyncText mText;
     private final Editable mProxy;
     private final ConcurrentLinkedQueue<Action> mActions;
     private KeyCharacterMap mKeyMap;
 
     // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables
     // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to
     // The two can be different when switching from one handler to another
@@ -80,18 +85,29 @@ import android.view.View;
     /* package */ SessionTextInput.EditableListener mListener;
 
     /* package */ boolean mInBatchMode; // Used by IC thread
     /* package */ boolean mNeedSync; // Used by IC thread
     // Gecko side needs an updated composition from Java;
     private boolean mNeedUpdateComposition; // Used by IC thread
     private boolean mSuppressKeyUp; // Used by IC thread
 
+    private int mIMEState = // Used by IC thread.
+            SessionTextInput.EditableListener.IME_STATE_DISABLED;
+    private String mIMETypeHint = ""; // Used by IC/UI thread.
+    private String mIMEModeHint = ""; // Used by IC thread.
+    private String mIMEActionHint = ""; // Used by IC thread.
+    private int mIMEFlags; // Used by IC thread.
+
     private boolean mIgnoreSelectionChange; // Used by Gecko thread
 
+    // Prevent showSoftInput and hideSoftInput from being called multiple times in a row,
+    // including reentrant calls on some devices. Used by UI/IC thread.
+    /* package */ final AtomicInteger mSoftInputReentrancyGuard = new AtomicInteger();
+
     private static final int IME_RANGE_CARETPOSITION = 1;
     private static final int IME_RANGE_RAWINPUT = 2;
     private static final int IME_RANGE_SELECTEDRAWTEXT = 3;
     private static final int IME_RANGE_CONVERTEDTEXT = 4;
     private static final int IME_RANGE_SELECTEDCONVERTEDTEXT = 5;
 
     private static final int IME_RANGE_LINE_NONE = 0;
     private static final int IME_RANGE_LINE_DOTTED = 1;
@@ -602,22 +618,23 @@ import android.view.View;
             if (DEBUG) {
                 Log.d(LOGTAG, "sending: " + event);
             }
             onKeyEvent(mFocusedChild, event, event.getAction(),
                        /* metaState */ 0, /* isSynthesizedImeKey */ true);
         }
     }
 
-    public GeckoEditable() {
+    public GeckoEditable(@NonNull final GeckoSession session) {
         if (DEBUG) {
             // Called by SessionTextInput.
             ThreadUtils.assertOnUiThread();
         }
 
+        mSession = session;
         mText = new AsyncText();
         mActions = new ConcurrentLinkedQueue<Action>();
 
         final Class<?>[] PROXY_INTERFACES = { Editable.class };
         mProxy = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(),
                                                    PROXY_INTERFACES, this);
 
         mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
@@ -1169,39 +1186,83 @@ import android.view.View;
                 // Only post to IC thread below when the queue is empty.
                 return;
             }
         }
 
         mIcPostHandler.post(new Runnable() {
             @Override
             public void run() {
-                if (type == SessionTextInput.EditableListener.NOTIFY_IME_REPLY_EVENT) {
-                    if (mNeedSync) {
-                        icSyncShadowText();
-                    }
-                    return;
-                }
-
-                if (type == SessionTextInput.EditableListener.NOTIFY_IME_OF_FOCUS &&
-                        mListener != null) {
-                    mFocusedChild = child;
-                    mNeedSync = false;
-                    mText.syncShadowText(/* listener */ null);
-                } else if (type == SessionTextInput.EditableListener.NOTIFY_IME_OF_BLUR) {
-                    mFocusedChild = null;
-                }
-
-                if (mListener != null) {
-                    mListener.notifyIME(type);
-                }
+                icNotifyIME(child, type);
             }
         });
     }
 
+    /* package */ void icNotifyIME(final IGeckoEditableChild child, final int type) {
+        if (DEBUG) {
+            assertOnIcThread();
+        }
+
+        if (type == SessionTextInput.EditableListener.NOTIFY_IME_REPLY_EVENT) {
+            if (mNeedSync) {
+                icSyncShadowText();
+            }
+            return;
+        }
+
+        switch (type) {
+            case SessionTextInput.EditableListener.NOTIFY_IME_OF_FOCUS:
+                mFocusedChild = child;
+                mNeedSync = false;
+                mText.syncShadowText(/* listener */ null);
+
+                // Do not reset mIMEState here; see comments in notifyIMEContext
+                if (mIMEState != SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+                    icRestartInput(SessionTextInput.Delegate.RESTART_REASON_FOCUS);
+                }
+                break;
+
+            case SessionTextInput.EditableListener.NOTIFY_IME_OF_BLUR:
+                mFocusedChild = null;
+                break;
+
+            case SessionTextInput.EditableListener.NOTIFY_IME_OPEN_VKB:
+                toggleSoftInput(/* force */ true);
+                return; // Don't notify listener.
+
+            case SessionTextInput.EditableListener.NOTIFY_IME_TO_COMMIT_COMPOSITION: {
+                // Gecko already committed its composition. However, Android keyboards
+                // have trouble dealing with us removing the composition manually on the
+                // Java side. Therefore, we keep the composition intact on the Java side.
+                // The text content should still be in-sync on both sides.
+                //
+                // Nevertheless, if we somehow lost the composition, we must force the
+                // keyboard to reset.
+                final Spanned text = mText.getShadowText();
+                final Object[] spans = text.getSpans(0, text.length(), Object.class);
+                for (final Object span : spans) {
+                    if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
+                        // Still have composition; no need to reset.
+                        return; // Don't notify listener.
+                    }
+                }
+                // No longer have composition; perform reset.
+                icRestartInput(SessionTextInput.Delegate.RESTART_REASON_CONTENT_CHANGE);
+                return; // Don't notify listener.
+            }
+
+            default:
+                throw new IllegalArgumentException("Invalid notifyIME type: " + type);
+        }
+
+        if (mListener != null) {
+            mListener.notifyIME(type);
+        }
+    }
+
     @Override // IGeckoEditableParent
     public void notifyIMEContext(final int state, final String typeHint,
                                  final String modeHint, final String actionHint,
                                  final int flags) {
         // On Gecko or binder thread.
         if (DEBUG) {
             Log.d(LOGTAG, "notifyIMEContext(" +
                           getConstantName(SessionTextInput.EditableListener.class,
@@ -1212,20 +1273,216 @@ import android.view.View;
 
         // Don't check token for notifyIMEContext, because the calls all come
         // from the parent process.
         ThreadUtils.assertOnGeckoThread();
 
         mIcPostHandler.post(new Runnable() {
             @Override
             public void run() {
-                if (mListener == null) {
+                icNotifyIMEContext(state, typeHint, modeHint, actionHint, flags);
+            }
+        });
+    }
+
+    /* package */ void icNotifyIMEContext(int state, final String typeHint,
+                                          final String modeHint, final String actionHint,
+                                          final int flags) {
+        if (DEBUG) {
+            assertOnIcThread();
+        }
+
+        // For some input type we will use a widget to display the ui, for those we must not
+        // display the ime. We can display a widget for date and time types and, if the sdk version
+        // is 11 or greater, for datetime/month/week as well.
+        if (typeHint != null && (typeHint.equalsIgnoreCase("date") ||
+                                 typeHint.equalsIgnoreCase("time") ||
+                                 typeHint.equalsIgnoreCase("month") ||
+                                 typeHint.equalsIgnoreCase("week") ||
+                                 typeHint.equalsIgnoreCase("datetime-local"))) {
+            state = SessionTextInput.EditableListener.IME_STATE_DISABLED;
+        }
+
+        final int oldState = mIMEState;
+        mIMEState = state;
+        mIMETypeHint = (typeHint == null) ? "" : typeHint;
+        mIMEModeHint = (modeHint == null) ? "" : modeHint;
+        mIMEActionHint = (actionHint == null) ? "" : actionHint;
+        mIMEFlags = flags;
+
+        if (mListener != null) {
+            mListener.notifyIMEContext(state, typeHint, modeHint, actionHint, flags);
+        }
+
+        // On focus, the notifyIMEContext call comes *before* the
+        // notifyIME(NOTIFY_IME_OF_FOCUS) call, but we need to call restartInput during
+        // notifyIME, so we skip restartInput here. On blur, the notifyIMEContext call
+        // comes *after* the notifyIME(NOTIFY_IME_OF_BLUR) call, and we need to call
+        // restartInput here.
+        if (oldState != SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+            if (state == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+                icRestartInput(SessionTextInput.Delegate.RESTART_REASON_BLUR);
+            } else if (mFocusedChild != null) {
+                icRestartInput(SessionTextInput.Delegate.RESTART_REASON_CONTENT_CHANGE);
+            }
+        }
+    }
+
+    private void icRestartInput(@SessionTextInput.Delegate.RestartReason final int reason) {
+        if (DEBUG) {
+            assertOnIcThread();
+        }
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mSoftInputReentrancyGuard.incrementAndGet();
+                mSession.getTextInput().getDelegate().restartInput(mSession, reason);
+
+                postToInputConnection(new Runnable() {
+                    @Override
+                    public void run() {
+                        toggleSoftInput(/* force */ false);
+                    }
+                });
+            }
+        });
+    }
+
+    public void onCreateInputConnection(final EditorInfo outAttrs) {
+        final int state = mIMEState;
+        final String typeHint = mIMETypeHint;
+        final String modeHint = mIMEModeHint;
+        final String actionHint = mIMEActionHint;
+        final int flags = mIMEFlags;
+
+        // Some keyboards require us to fill out outAttrs even if we return null.
+        outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
+        outAttrs.actionLabel = null;
+
+        if (state == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+            outAttrs.inputType = InputType.TYPE_NULL;
+            toggleSoftInput(/* force */ false);
+            return;
+        }
+
+        outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
+        if (state == SessionTextInput.EditableListener.IME_STATE_PASSWORD ||
+                "password".equalsIgnoreCase(typeHint)) {
+            outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
+        } else if (typeHint.equalsIgnoreCase("url") ||
+                typeHint.equalsIgnoreCase("mozAwesomebar")) {
+            outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
+        } else if (typeHint.equalsIgnoreCase("email")) {
+            outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+        } else if (typeHint.equalsIgnoreCase("tel")) {
+            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
+        } else if (typeHint.equalsIgnoreCase("number") ||
+                typeHint.equalsIgnoreCase("range")) {
+            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
+                    | InputType.TYPE_NUMBER_FLAG_SIGNED
+                    | InputType.TYPE_NUMBER_FLAG_DECIMAL;
+        } else if (modeHint.equalsIgnoreCase("numeric")) {
+            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
+                    InputType.TYPE_NUMBER_FLAG_SIGNED |
+                    InputType.TYPE_NUMBER_FLAG_DECIMAL;
+        } else if (modeHint.equalsIgnoreCase("digit")) {
+            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
+        } else {
+            // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
+            outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
+                    InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
+            if (typeHint.equalsIgnoreCase("textarea") ||
+                    typeHint.length() == 0) {
+                // empty typeHint indicates contentEditable/designMode documents
+                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+            }
+            if (modeHint.equalsIgnoreCase("uppercase")) {
+                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+            } else if (modeHint.equalsIgnoreCase("titlecase")) {
+                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
+            } else if (typeHint.equalsIgnoreCase("text") &&
+                    !modeHint.equalsIgnoreCase("autocapitalized")) {
+                outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
+            } else if (!modeHint.equalsIgnoreCase("lowercase")) {
+                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+            }
+            // auto-capitalized mode is the default for types other than text
+        }
+
+        if (actionHint.equalsIgnoreCase("go")) {
+            outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
+        } else if (actionHint.equalsIgnoreCase("done")) {
+            outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+        } else if (actionHint.equalsIgnoreCase("next")) {
+            outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
+        } else if (actionHint.equalsIgnoreCase("search") ||
+                typeHint.equalsIgnoreCase("search")) {
+            outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
+        } else if (actionHint.equalsIgnoreCase("send")) {
+            outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
+        } else if (actionHint.length() > 0) {
+            if (DEBUG)
+                Log.w(LOGTAG, "Unexpected actionHint=\"" + actionHint + "\"");
+            outAttrs.actionLabel = actionHint;
+        }
+
+        if ((flags & SessionTextInput.EditableListener.IME_FLAG_PRIVATE_BROWSING) != 0) {
+            outAttrs.imeOptions |= InputMethods.IME_FLAG_NO_PERSONALIZED_LEARNING;
+        }
+
+        toggleSoftInput(/* force */ false);
+    }
+
+    /* package */ void toggleSoftInput(final boolean force) {
+        // Can be called from UI or IC thread.
+        final int state = mIMEState;
+        final int flags = mIMEFlags;
+
+        // There are three paths that toggleSoftInput() can be called:
+        // 1) through calling restartInput(), which then indirectly calls
+        //    onCreateInputConnection() and then toggleSoftInput().
+        // 2) through calling toggleSoftInput() directly from restartInput().
+        //    This path is the fallback in case 1) does not happen.
+        // 3) through a system-generated onCreateInputConnection() call when the activity
+        //    is restored from background, which then calls toggleSoftInput().
+        // mSoftInputReentrancyGuard is needed to ensure that between the different paths,
+        // the soft input is only toggled exactly once.
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final int reentrancyGuard = mSoftInputReentrancyGuard.decrementAndGet();
+                final boolean isReentrant;
+                if (reentrancyGuard < 0) {
+                    mSoftInputReentrancyGuard.incrementAndGet();
+                    isReentrant = false;
+                } else {
+                    isReentrant = reentrancyGuard > 0;
+                }
+
+                // When using Find In Page, we can still receive notifyIMEContext calls due to the
+                // selection changing when highlighting. However in this case we don't want to
+                // show/hide the keyboard because the find box has the focus and is taking input from
+                // the keyboard.
+                final View view = mSession.getTextInput().getView();
+                final boolean isFocused = (view == null) || view.hasFocus();
+
+                final boolean isUserAction = ((flags &
+                        SessionTextInput.EditableListener.IME_FLAG_USER_ACTION) != 0);
+
+                if (!force && (isReentrant || !isFocused || !isUserAction)) {
                     return;
                 }
-                mListener.notifyIMEContext(state, typeHint, modeHint, actionHint, flags);
+                if (state == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+                    mSession.getTextInput().getDelegate().hideSoftInput(mSession);
+                    return;
+                }
+                mSession.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
+                mSession.getTextInput().getDelegate().showSoftInput(mSession);
             }
         });
     }
 
     @Override // IGeckoEditableParent
     public void onSelectionChange(final IBinder token,
                                   final int start, final int end) {
         // On Gecko or binder thread.
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoInputConnection.java
@@ -1,45 +1,41 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview;
 
 import android.annotation.TargetApi;
-import android.app.Activity;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Matrix;
 import android.graphics.RectF;
 import android.media.AudioManager;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.support.annotation.NonNull;
 import android.text.Editable;
-import android.text.InputType;
 import android.text.Selection;
 import android.text.SpannableString;
-import android.text.Spanned;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 
 import org.mozilla.gecko.Clipboard;
 import org.mozilla.gecko.InputMethods;
-import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 
 /* package */ final class GeckoInputConnection
     extends BaseInputConnection
@@ -54,37 +50,30 @@ import java.lang.reflect.Proxy;
         "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
 
     private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
 
     private static Handler sBackgroundHandler;
 
     // Managed only by notifyIMEContext; see comments in notifyIMEContext
     private int mIMEState;
-    private String mIMETypeHint = "";
-    private String mIMEModeHint = "";
     private String mIMEActionHint = "";
-    private int mIMEFlags;
-    private boolean mFocused;
     private int mLastSelectionStart;
     private int mLastSelectionEnd;
 
     private String mCurrentInputMethod = "";
 
     private final GeckoSession mSession;
     private final View mView;
     private final SessionTextInput.EditableClient mEditableClient;
     protected int mBatchEditCount;
     private ExtractedTextRequest mUpdateRequest;
     private final InputConnection mKeyInputConnection;
     private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
 
-    // Prevent showSoftInput and hideSoftInput from causing reentrant calls on some devices.
-    private volatile boolean mSoftInputReentrancyGuard;
-
     public static SessionTextInput.InputConnectionClient create(
             final GeckoSession session,
             final View targetView,
             final SessionTextInput.EditableClient editable) {
         SessionTextInput.InputConnectionClient ic = new GeckoInputConnection(session, targetView, editable);
         if (DEBUG) {
             ic = wrapForDebug(ic);
         }
@@ -270,59 +259,16 @@ import java.lang.reflect.Proxy;
         return mView;
     }
 
     @NonNull
     /* package */ SessionTextInput.Delegate getInputDelegate() {
         return mSession.getTextInput().getDelegate();
     }
 
-    private void showSoftInputWithToolbar(final boolean showToolbar) {
-        if (mSoftInputReentrancyGuard) {
-            return;
-        }
-
-        getView().post(new Runnable() {
-            @Override
-            public void run() {
-                if (showToolbar) {
-                    mSession.getDynamicToolbarAnimator().showToolbar(/* immediately */ true);
-                }
-                mSession.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
-
-                mSoftInputReentrancyGuard = true;
-                getInputDelegate().showSoftInput(mSession);
-                mSoftInputReentrancyGuard = false;
-            }
-        });
-    }
-
-    private void hideSoftInput() {
-        if (mSoftInputReentrancyGuard) {
-            return;
-        }
-        getView().post(new Runnable() {
-            @Override
-            public void run() {
-                mSoftInputReentrancyGuard = true;
-                getInputDelegate().hideSoftInput(mSession);
-                mSoftInputReentrancyGuard = false;
-            }
-        });
-    }
-
-    private void restartInput(final @SessionTextInput.Delegate.RestartReason int reason) {
-        getView().post(new Runnable() {
-            @Override
-            public void run() {
-                getInputDelegate().restartInput(mSession, reason);
-            }
-        });
-    }
-
     @Override // SessionTextInput.EditableListener
     public void onTextChange() {
         final Editable editable = getEditable();
         if (mUpdateRequest == null || editable == null) {
             return;
         }
 
         final ExtractedTextRequest request = mUpdateRequest;
@@ -561,93 +507,20 @@ import java.lang.reflect.Proxy;
     @Override // InputConnection
     public void closeConnection() {
         // Not supported at the moment.
         super.closeConnection();
     }
 
     @Override // SessionTextInput.InputConnectionClient
     public synchronized InputConnection onCreateInputConnection(EditorInfo outAttrs) {
-        // Some keyboards require us to fill out outAttrs even if we return null.
-        outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
-        outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
-        outAttrs.actionLabel = null;
-
         if (mIMEState == IME_STATE_DISABLED) {
-            hideSoftInput();
             return null;
         }
 
-        if (mIMEState == IME_STATE_PASSWORD ||
-            "password".equalsIgnoreCase(mIMETypeHint))
-            outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
-        else if (mIMETypeHint.equalsIgnoreCase("url") ||
-                 mIMETypeHint.equalsIgnoreCase("mozAwesomebar"))
-            outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_URI;
-        else if (mIMETypeHint.equalsIgnoreCase("email"))
-            outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
-        else if (mIMETypeHint.equalsIgnoreCase("tel"))
-            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
-        else if (mIMETypeHint.equalsIgnoreCase("number") ||
-                 mIMETypeHint.equalsIgnoreCase("range"))
-            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
-                                 | InputType.TYPE_NUMBER_FLAG_SIGNED
-                                 | InputType.TYPE_NUMBER_FLAG_DECIMAL;
-        else if (mIMETypeHint.equalsIgnoreCase("week") ||
-                 mIMETypeHint.equalsIgnoreCase("month"))
-            outAttrs.inputType = InputType.TYPE_CLASS_DATETIME
-                                  | InputType.TYPE_DATETIME_VARIATION_DATE;
-        else if (mIMEModeHint.equalsIgnoreCase("numeric"))
-            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
-                                 InputType.TYPE_NUMBER_FLAG_SIGNED |
-                                 InputType.TYPE_NUMBER_FLAG_DECIMAL;
-        else if (mIMEModeHint.equalsIgnoreCase("digit"))
-            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
-        else {
-            // TYPE_TEXT_FLAG_IME_MULTI_LINE flag makes the fullscreen IME line wrap
-            outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT |
-                                  InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE;
-            if (mIMETypeHint.equalsIgnoreCase("textarea") ||
-                    mIMETypeHint.length() == 0) {
-                // empty mIMETypeHint indicates contentEditable/designMode documents
-                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
-            }
-            if (mIMEModeHint.equalsIgnoreCase("uppercase"))
-                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
-            else if (mIMEModeHint.equalsIgnoreCase("titlecase"))
-                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
-            else if (mIMETypeHint.equalsIgnoreCase("text") &&
-                    !mIMEModeHint.equalsIgnoreCase("autocapitalized"))
-                outAttrs.inputType |= InputType.TYPE_TEXT_VARIATION_NORMAL;
-            else if (!mIMEModeHint.equalsIgnoreCase("lowercase"))
-                outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
-            // auto-capitalized mode is the default for types other than text
-        }
-
-        if (mIMEActionHint.equalsIgnoreCase("go"))
-            outAttrs.imeOptions = EditorInfo.IME_ACTION_GO;
-        else if (mIMEActionHint.equalsIgnoreCase("done"))
-            outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
-        else if (mIMEActionHint.equalsIgnoreCase("next"))
-            outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
-        else if (mIMEActionHint.equalsIgnoreCase("search") ||
-                 mIMETypeHint.equalsIgnoreCase("search"))
-            outAttrs.imeOptions = EditorInfo.IME_ACTION_SEARCH;
-        else if (mIMEActionHint.equalsIgnoreCase("send"))
-            outAttrs.imeOptions = EditorInfo.IME_ACTION_SEND;
-        else if (mIMEActionHint.length() > 0) {
-            if (DEBUG)
-                Log.w(LOGTAG, "Unexpected mIMEActionHint=\"" + mIMEActionHint + "\"");
-            outAttrs.actionLabel = mIMEActionHint;
-        }
-
-        if ((mIMEFlags & IME_FLAG_PRIVATE_BROWSING) != 0) {
-            outAttrs.imeOptions |= InputMethods.IME_FLAG_NO_PERSONALIZED_LEARNING;
-        }
-
         Context context = getView().getContext();
         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
         if (Math.min(metrics.widthPixels, metrics.heightPixels) > INLINE_IME_MIN_DISPLAY_SIZE) {
             // prevent showing full-screen keyboard only when the screen is tall enough
             // to show some reasonable amount of the page (see bug 752709)
             outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI
                                    | EditorInfo.IME_FLAG_NO_FULLSCREEN;
         }
@@ -661,25 +534,16 @@ import java.lang.reflect.Proxy;
         String prevInputMethod = mCurrentInputMethod;
         mCurrentInputMethod = InputMethods.getCurrentInputMethod(context);
         if (DEBUG) {
             Log.d(LOGTAG, "IME: CurrentInputMethod=" + mCurrentInputMethod);
         }
 
         outAttrs.initialSelStart = mLastSelectionStart;
         outAttrs.initialSelEnd = mLastSelectionEnd;
-
-        if ((mIMEFlags & IME_FLAG_USER_ACTION) != 0) {
-            if ((context instanceof Activity) &&
-                    ActivityUtils.isFullScreen((Activity) context)) {
-                showSoftInputWithToolbar(false);
-            } else {
-                showSoftInputWithToolbar(true);
-            }
-        }
         return this;
     }
 
     private boolean replaceComposingSpanWithSelection() {
         final Editable content = getEditable();
         if (content == null) {
             return false;
         }
@@ -775,120 +639,50 @@ import java.lang.reflect.Proxy;
 
     @Override // SessionTextInput.InputConnectionClient
     public synchronized boolean isInputActive() {
         // Make sure this picks up PASSWORD state as well.
         return mIMEState != IME_STATE_DISABLED;
     }
 
     @Override // SessionTextInput.EditableListener
-    public void notifyIME(int type) {
+    public void notifyIME(final int type) {
         switch (type) {
-
             case NOTIFY_IME_OF_FOCUS:
                 // Showing/hiding vkb is done in notifyIMEContext
-                mFocused = true;
                 if (mBatchEditCount != 0) {
                     Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
                     mBatchEditCount = 0;
                 }
-                // Do not reset mIMEState here; see comments in notifyIMEContext
-                restartInput(SessionTextInput.Delegate.RESTART_REASON_FOCUS);
                 break;
 
             case NOTIFY_IME_OF_BLUR:
-                // Showing/hiding vkb is done in notifyIMEContext
-                mFocused = false;
                 break;
 
-            case NOTIFY_IME_OPEN_VKB:
-                showSoftInputWithToolbar(false);
-                break;
-
-            case NOTIFY_IME_TO_COMMIT_COMPOSITION: {
-                // Gecko already committed its composition. However, Android keyboards
-                // have trouble dealing with us removing the composition manually on the
-                // Java side. Therefore, we keep the composition intact on the Java side.
-                // The text content should still be in-sync on both sides.
-                //
-                // Nevertheless, if we somehow lost the composition, we must force the
-                // keyboard to reset.
-                final Editable editable = getEditable();
-                if (editable == null) {
-                    break;
-                }
-                final Object[] spans = editable.getSpans(0, editable.length(), Object.class);
-                for (final Object span : spans) {
-                    if ((editable.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
-                        // Still have composition; no need to reset.
-                        return;
-                    }
-                }
-                // No longer have composition; perform reset.
-                restartInput(SessionTextInput.Delegate.RESTART_REASON_CONTENT_CHANGE);
-                break;
-            }
-
             default:
                 if (DEBUG) {
                     throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
                 }
                 break;
         }
     }
 
     @Override // SessionTextInput.EditableListener
-    public synchronized void notifyIMEContext(int state, final String typeHint,
+    public synchronized void notifyIMEContext(final int state, final String typeHint,
                                               final String modeHint, final String actionHint,
                                               final int flags) {
-        // For some input type we will use a widget to display the ui, for those we must not
-        // display the ime. We can display a widget for date and time types and, if the sdk version
-        // is 11 or greater, for datetime/month/week as well.
-        if (typeHint != null &&
-            (typeHint.equalsIgnoreCase("date") ||
-             typeHint.equalsIgnoreCase("time") ||
-             typeHint.equalsIgnoreCase("datetime") ||
-             typeHint.equalsIgnoreCase("month") ||
-             typeHint.equalsIgnoreCase("week") ||
-             typeHint.equalsIgnoreCase("datetime-local"))) {
-            state = IME_STATE_DISABLED;
-        }
-
         // mIMEState and the mIME*Hint fields should only be changed by notifyIMEContext,
         // and not reset anywhere else. Usually, notifyIMEContext is called right after a
         // focus or blur, so resetting mIMEState during the focus or blur seems harmless.
         // However, this behavior is not guaranteed. Gecko may call notifyIMEContext
         // independent of focus change; that is, a focus change may not be accompanied by
         // a notifyIMEContext call. So if we reset mIMEState inside focus, there may not
         // be another notifyIMEContext call to set mIMEState to a proper value (bug 829318)
         /* When IME is 'disabled', IME processing is disabled.
            In addition, the IME UI is hidden */
         mIMEState = state;
-        mIMETypeHint = (typeHint == null) ? "" : typeHint;
-        mIMEModeHint = (modeHint == null) ? "" : modeHint;
         mIMEActionHint = (actionHint == null) ? "" : actionHint;
-        mIMEFlags = flags;
 
         // These fields are reset here and will be updated when restartInput is called below
         mUpdateRequest = null;
         mCurrentInputMethod = "";
-
-        View v = getView();
-        if (v == null || !v.hasFocus()) {
-            // When using Find In Page, we can still receive notifyIMEContext calls due to the
-            // selection changing when highlighting. However in this case we don't want to reset/
-            // show/hide the keyboard because the find box has the focus and is taking input from
-            // the keyboard.
-            return;
-        }
-
-        // On focus, the notifyIMEContext call comes *before* the
-        // notifyIME(NOTIFY_IME_OF_FOCUS) call, but we need to call restartInput during
-        // notifyIME, so we skip restartInput here. On blur, the notifyIMEContext call
-        // comes *after* the notifyIME(NOTIFY_IME_OF_BLUR) call, and we need to call
-        // restartInput here.
-        if (mIMEState == IME_STATE_DISABLED || mFocused) {
-            restartInput(mIMEState == IME_STATE_DISABLED ?
-                         SessionTextInput.Delegate.RESTART_REASON_BLUR :
-                         SessionTextInput.Delegate.RESTART_REASON_CONTENT_CHANGE);
-        }
     }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
@@ -5,19 +5,21 @@
 
 package org.mozilla.geckoview;
 
 import org.mozilla.gecko.InputMethods;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.GeckoEditableChild;
 import org.mozilla.gecko.IGeckoEditableParent;
 import org.mozilla.gecko.NativeQueue;
+import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.TargetApi;
+import android.app.Activity;
 import android.content.Context;
 import android.graphics.RectF;
 import android.os.Handler;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.Editable;
 import android.util.Log;
@@ -29,28 +31,34 @@ import android.view.inputmethod.Extracte
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * SessionTextInput handles text input for GeckoSession through key events or input
- * methods. It is typically used to implement certain methods in View such as {@code
- * onCreateInputConnection()}, by forwarding such calls to corresponding methods in
- * SessionTextInput.
+ * {@code SessionTextInput} handles text input for {@code GeckoSession} through key events or input
+ * methods. It is typically used to implement certain methods in {@link android.view.View}
+ * such as {@link android.view.View#onCreateInputConnection}, by forwarding such calls to
+ * corresponding methods in {@code SessionTextInput}.
+ * <p>
+ * For full functionality, {@code SessionTextInput} requires a {@link android.view.View} to be set
+ * first through {@link #setView}. When a {@link android.view.View} is not set or set to null,
+ * {@code SessionTextInput} will operate in a reduced functionality mode. See {@link
+ * #onCreateInputConnection} and methods in {@link Delegate} for changes in behavior in this
+ * viewless mode.
  */
 public final class SessionTextInput {
     /* package */ static final String LOGTAG = "GeckoSessionTextInput";
 
     /**
      * Interface that SessionTextInput uses for performing operations such as opening and closing
      * the software keyboard. If the delegate is not set, these operations are forwarded to the
-     * system InputMethodManager automatically.
+     * system {@link android.view.inputmethod.InputMethodManager} automatically.
      */
     public interface Delegate {
         @Retention(RetentionPolicy.SOURCE)
         @IntDef({RESTART_REASON_FOCUS, RESTART_REASON_BLUR, RESTART_REASON_CONTENT_CHANGE})
         @interface RestartReason {}
         /** Restarting input due to an input field gaining focus. */
         int RESTART_REASON_FOCUS = 0;
         /** Restarting input due to an input field losing focus. */
@@ -64,66 +72,72 @@ public final class SessionTextInput {
 
         /**
          * Reset the input method, and discard any existing states such as the current composition
          * or current autocompletion. Because the current focused editor may have changed, as
          * part of the reset, a custom input method would normally call {@link
          * #onCreateInputConnection} to update its knowledge of the focused editor. Note that
          * {@code restartInput} should be used to detect changes in focus, rather than {@link
          * #showSoftInput} or {@link #hideSoftInput}, because focus changes are not always
-         * accompanied by requests to show or hide the soft input.
+         * accompanied by requests to show or hide the soft input. This method is always called,
+         * even in viewless mode.
          *
          * @param session Session instance.
          * @param reason Reason for the reset.
          */
         void restartInput(@NonNull GeckoSession session, @RestartReason int reason);
 
         /**
-         * Display the soft input.
+         * Display the soft input. May be called consecutively, even if the soft input is
+         * already shown. This method is always called, even in viewless mode.
          *
          * @param session Session instance.
          * @see #hideSoftInput
          * */
         void showSoftInput(@NonNull GeckoSession session);
 
         /**
-         * Hide the soft input.
+         * Hide the soft input. May be called consecutively, even if the soft input is
+         * already hidden. This method is always called, even in viewless mode.
          *
          * @param session Session instance.
          * @see #showSoftInput
          * */
         void hideSoftInput(@NonNull GeckoSession session);
 
         /**
-         * Update the soft input on the current selection.
+         * Update the soft input on the current selection. This method is <i>not</i> called
+         * in viewless mode.
          *
          * @param session Session instance.
          * @param selStart Start offset of the selection.
          * @param selEnd End offset of the selection.
          * @param compositionStart Composition start offset, or -1 if there is no composition.
          * @param compositionEnd Composition end offset, or -1 if there is no composition.
          */
         void updateSelection(@NonNull GeckoSession session, int selStart, int selEnd,
                              int compositionStart, int compositionEnd);
 
         /**
-         * Update the soft input on the current extracted text as requested through
-         * InputConnection.getExtractText.
+         * Update the soft input on the current extracted text, as requested through
+         * {@link android.view.inputmethod.InputConnection#getExtractedText}.
+         * Consequently, this method is <i>not</i> called in viewless mode.
          *
          * @param session Session instance.
          * @param request The extract text request.
          * @param text The extracted text.
          */
         void updateExtractedText(@NonNull GeckoSession session,
                                  @NonNull ExtractedTextRequest request,
                                  @NonNull ExtractedText text);
 
         /**
          * Update the cursor-anchor information as requested through
-         * InputConnection.requestCursorUpdates.
+         * {@link android.view.inputmethod.InputConnection#requestCursorUpdates}.
+         * Consequently, this method is <i>not</i> called in viewless mode.
          *
          * @param session Session instance.
          * @param info Cursor-anchor information.
          */
         void updateCursorAnchorInfo(@NonNull GeckoSession session, @NonNull CursorAnchorInfo info);
     }
 
     // Interface to access GeckoInputConnection from SessionTextInput.
@@ -194,16 +208,27 @@ public final class SessionTextInput {
             return (InputMethodManager) view.getContext()
                                             .getSystemService(Context.INPUT_METHOD_SERVICE);
         }
 
         @Override
         public void restartInput(@NonNull final GeckoSession session, final int reason) {
             ThreadUtils.assertOnUiThread();
             final View view = session.getTextInput().getView();
+
+            if (reason == RESTART_REASON_FOCUS) {
+                final Context context = (view != null) ? view.getContext() : null;
+                if ((context instanceof Activity) &&
+                        !ActivityUtils.isFullScreen((Activity) context)) {
+                    // Bug 1293463: show the toolbar to prevent spoofing.
+                    session.getDynamicToolbarAnimator()
+                           .showToolbar(/* immediately */ true);
+                }
+            }
+
             final InputMethodManager imm = getInputMethodManager(view);
             if (imm == null) {
                 return;
             }
 
             // InputMethodManager has internal logic to detect if we are restarting input
             // in an already focused View, which is the case here because all content text
             // fields are inside one LayerView. When this happens, InputMethodManager will
@@ -287,25 +312,27 @@ public final class SessionTextInput {
             if (imm != null) {
                 imm.updateCursorAnchorInfo(view, info);
             }
         }
     }
 
     private final GeckoSession mSession;
     private final NativeQueue mQueue;
-    private final GeckoEditable mEditable = new GeckoEditable();
-    private final GeckoEditableChild mEditableChild = new GeckoEditableChild(mEditable);
+    private final GeckoEditable mEditable;
+    private final GeckoEditableChild mEditableChild;
     private InputConnectionClient mInputConnection;
     private Delegate mDelegate;
 
     /* package */ SessionTextInput(final @NonNull GeckoSession session,
                                    final @NonNull NativeQueue queue) {
         mSession = session;
         mQueue = queue;
+        mEditable = new GeckoEditable(session);
+        mEditableChild = new GeckoEditableChild(mEditable);
         mEditable.setDefaultEditableChild(mEditableChild);
     }
 
     /* package */ void onWindowChanged(final GeckoSession.Window window) {
         if (mQueue.isReady()) {
             window.attachEditable(mEditable, mEditableChild);
         } else {
             mQueue.queueUntilReady(window, "attachEditable",
@@ -336,53 +363,60 @@ public final class SessionTextInput {
         // May be called on any thread.
         if (mInputConnection != null) {
             return mInputConnection.getHandler(defHandler);
         }
         return defHandler;
     }
 
     /**
-     * Get the current View for text input.
+     * Get the current {@link android.view.View} for text input.
      *
      * @return Current text input View or null if not set.
      * @see #setView(View)
      */
     public @Nullable View getView() {
         ThreadUtils.assertOnUiThread();
         return mInputConnection != null ? mInputConnection.getView() : null;
     }
 
     /**
-     * Set the View for text input. The current View is used to interact with the system
-     * input method manager and to display certain text input UI elements.
+     * Set the current {@link android.view.View} for text input. The {@link android.view.View}
+     * is used to interact with the system input method manager and to display certain text input
+     * UI elements. See the {@code SessionTextInput} class documentation for information on
+     * viewless mode, when the current {@link android.view.View} is not set or set to null.
      *
      * @param view Text input View or null to clear current View.
+     * @see #getView()
      */
     public synchronized void setView(final @Nullable View view) {
         ThreadUtils.assertOnUiThread();
 
         if (view == null) {
             mInputConnection = null;
         } else if (mInputConnection == null || mInputConnection.getView() != view) {
             mInputConnection = GeckoInputConnection.create(mSession, view, mEditable);
         }
         mEditable.setListener((EditableListener) mInputConnection);
     }
 
     /**
-     * Get an InputConnection instance. For full functionality, call {@link
-     * #setView(View)} first before calling this method.
+     * Get an {@link android.view.inputmethod.InputConnection} instance. In viewless mode,
+     * this method still fills out the {@link android.view.inputmethod.EditorInfo} object,
+     * but the return value will always be null.
      *
      * @param attrs EditorInfo instance to be filled on return.
-     * @return InputConnection instance or null if input method is not active.
+     * @return InputConnection instance, or null if there is no active input
+     *         (or if in viewless mode).
      */
     public synchronized @Nullable InputConnection onCreateInputConnection(
             final @NonNull EditorInfo attrs) {
         // May be called on any thread.
+        mEditable.onCreateInputConnection(attrs);
+
         if (!mQueue.isReady() || mInputConnection == null) {
             return null;
         }
         return mInputConnection.onCreateInputConnection(attrs);
     }
 
     /**
      * Process a KeyEvent as a pre-IME event.