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
--- 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.