Bug 1466910 - 5. Correctly handle focus/blur ordering issues; r?esawin draft
authorJim Chen <nchen@mozilla.com>
Tue, 19 Jun 2018 16:31:34 -0400
changeset 808551 947ddec6046f10c8344455924320c4e03ab0def8
parent 808550 a01f2352eb01b08346ca4749c13032d7223693c4
child 808552 0b9366828e1c422ecfa70cf12ff30f761308d51e
push id113421
push userbmo:nchen@mozilla.com
push dateTue, 19 Jun 2018 22:59:14 +0000
reviewersesawin
bugs1466910
milestone62.0a1
Bug 1466910 - 5. Correctly handle focus/blur ordering issues; r?esawin * Move restartInput(blur) call from notifyIMEContext to notifyIME, so we don't make the call unnecessarily. * Correctly handle when notifyIMEContext(enabled) is called _after_ notifyIME(focus), by having notifyIMEContext call restartInput(focus) if necessary. MozReview-Commit-ID: 4jJlmhOws8
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoEditable.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
@@ -1204,32 +1204,47 @@ import android.view.inputmethod.EditorIn
             if (mNeedSync) {
                 icSyncShadowText();
             }
             return;
         }
 
         switch (type) {
             case SessionTextInput.EditableListener.NOTIFY_IME_OF_FOCUS:
+                if (mFocusedChild != null) {
+                    // Already focused, so blur first.
+                    icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_BLUR,
+                                   /* toggleSoftInput */ false);
+                }
+
                 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(GeckoSession.TextInputDelegate.RESTART_REASON_FOCUS);
+                // Most of the time notifyIMEContext comes _before_ notifyIME, but sometimes it
+                // comes _after_ notifyIME. In that case, the state is disabled here, and
+                // notifyIMEContext is responsible for calling restartInput.
+                if (mIMEState == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+                    mIMEState = SessionTextInput.EditableListener.IME_STATE_UNKNOWN;
+                } else {
+                    icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_FOCUS,
+                                   /* toggleSoftInput */ true);
                 }
                 break;
 
             case SessionTextInput.EditableListener.NOTIFY_IME_OF_BLUR:
-                mFocusedChild = null;
+                if (mFocusedChild != null) {
+                    mFocusedChild = null;
+                    icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_BLUR,
+                                   /* toggleSoftInput */ true);
+                }
                 break;
 
             case SessionTextInput.EditableListener.NOTIFY_IME_OPEN_VKB:
-                toggleSoftInput(/* force */ true);
+                toggleSoftInput(/* force */ true, mIMEState);
                 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.
                 //
@@ -1239,17 +1254,18 @@ import android.view.inputmethod.EditorIn
                 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(GeckoSession.TextInputDelegate.RESTART_REASON_CONTENT_CHANGE);
+                icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_CONTENT_CHANGE,
+                               /* toggleSoftInput */ false);
                 return; // Don't notify listener.
             }
 
             default:
                 throw new IllegalArgumentException("Invalid notifyIME type: " + type);
         }
 
         if (mListener != null) {
@@ -1306,48 +1322,65 @@ import android.view.inputmethod.EditorIn
         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.
-
-        // In either case, there is nothing to do here if we were disabled before.
-        if (oldState == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+        if (state == SessionTextInput.EditableListener.IME_STATE_DISABLED ||
+                mFocusedChild == null) {
             return;
         }
-        if (state == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
-            icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_BLUR);
-        } else if (mFocusedChild != null) {
-            icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_CONTENT_CHANGE);
+
+        // We changed state while focused. If the old state is unknown, it means this
+        // notifyIMEContext call came _after_ the notifyIME call, so we need to call
+        // restartInput(FOCUS) here (see comment in icNotifyIME). Otherwise, this change
+        // counts as a content change.
+        if (oldState == SessionTextInput.EditableListener.IME_STATE_UNKNOWN) {
+            icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_FOCUS,
+                           /* toggleSoftInput */ true);
+        } else if (oldState != SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+            icRestartInput(GeckoSession.TextInputDelegate.RESTART_REASON_CONTENT_CHANGE,
+                           /* toggleSoftInput */ false);
         }
     }
 
-    private void icRestartInput(@GeckoSession.TextInputDelegate.RestartReason final int reason) {
+    private void icRestartInput(@GeckoSession.TextInputDelegate.RestartReason final int reason,
+                                final boolean toggleSoftInput) {
         if (DEBUG) {
             assertOnIcThread();
         }
 
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
-                mSoftInputReentrancyGuard.incrementAndGet();
+                if (DEBUG) {
+                    Log.d(LOGTAG, "restartInput(" + reason + ", " + toggleSoftInput + ')');
+                }
+                if (toggleSoftInput) {
+                    mSoftInputReentrancyGuard.incrementAndGet();
+                }
                 mSession.getTextInput().getDelegate().restartInput(mSession, reason);
 
+                if (!toggleSoftInput) {
+                    return;
+                }
                 postToInputConnection(new Runnable() {
                     @Override
                     public void run() {
-                        toggleSoftInput(/* force */ false);
+                        int state = mIMEState;
+                        if (reason == GeckoSession.TextInputDelegate.RESTART_REASON_BLUR &&
+                                    mFocusedChild == null) {
+                            // On blur, notifyIMEContext() is called after notifyIME(). Therefore,
+                            // mIMEState is not up-to-date here and we need to override it.
+                            state = SessionTextInput.EditableListener.IME_STATE_DISABLED;
+                        }
+                        toggleSoftInput(/* force */ false, state);
                     }
                 });
             }
         });
     }
 
     public void onCreateInputConnection(final EditorInfo outAttrs) {
         final int state = mIMEState;
@@ -1357,17 +1390,17 @@ import android.view.inputmethod.EditorIn
         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);
+            toggleSoftInput(/* force */ false, state);
             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") ||
@@ -1426,22 +1459,24 @@ import android.view.inputmethod.EditorIn
                 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);
+        toggleSoftInput(/* force */ false, state);
     }
 
-    /* package */ void toggleSoftInput(final boolean force) {
+    /* package */ void toggleSoftInput(final boolean force, final int state) {
+        if (DEBUG) {
+            Log.d(LOGTAG, "toggleSoftInput");
+        }
         // 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
@@ -1467,22 +1502,32 @@ import android.view.inputmethod.EditorIn
                 // 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)) {
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "toggleSoftInput: no-op, reentrant=" + isReentrant +
+                                ", focused=" + isFocused + ", user=" + isUserAction);
+                    }
                     return;
                 }
                 if (state == SessionTextInput.EditableListener.IME_STATE_DISABLED) {
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "hideSoftInput");
+                    }
                     mSession.getTextInput().getDelegate().hideSoftInput(mSession);
                     return;
                 }
+                if (DEBUG) {
+                    Log.d(LOGTAG, "showSoftInput");
+                }
                 mSession.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
                 mSession.getTextInput().getDelegate().showSoftInput(mSession);
             }
         });
     }
 
     @Override // IGeckoEditableParent
     public void onSelectionChange(final IBinder token,
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
@@ -80,16 +80,17 @@ public final class SessionTextInput {
         @WrapForJNI final int NOTIFY_IME_OPEN_VKB = -2;
         @WrapForJNI final int NOTIFY_IME_REPLY_EVENT = -1;
         @WrapForJNI final int NOTIFY_IME_OF_FOCUS = 1;
         @WrapForJNI final int NOTIFY_IME_OF_BLUR = 2;
         @WrapForJNI final int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
         @WrapForJNI final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
 
         // IME enabled state for notifyIMEContext().
+        final int IME_STATE_UNKNOWN = -1;
         final int IME_STATE_DISABLED = 0;
         final int IME_STATE_ENABLED = 1;
         final int IME_STATE_PASSWORD = 2;
 
         // Flags for notifyIMEContext().
         @WrapForJNI final int IME_FLAG_PRIVATE_BROWSING = 1;
         @WrapForJNI final int IME_FLAG_USER_ACTION = 2;