summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java1589
1 files changed, 1589 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
new file mode 100644
index 000000000..695cff443
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
@@ -0,0 +1,1589 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
+
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.NoCopySpan;
+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.style.CharacterStyle;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+
+/*
+ GeckoEditable implements only some functions of Editable
+ The field mText contains the actual underlying
+ SpannableStringBuilder/Editable that contains our text.
+*/
+final class GeckoEditable extends JNIObject
+ implements InvocationHandler, Editable, GeckoEditableClient {
+
+ private static final boolean DEBUG = false;
+ private static final String LOGTAG = "GeckoEditable";
+
+ // Filters to implement Editable's filtering functionality
+ private InputFilter[] mFilters;
+
+ 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
+ private Handler mIcRunHandler;
+ private Handler mIcPostHandler;
+
+ /* package */ GeckoEditableListener mListener;
+ /* package */ GeckoView mView;
+
+ /* 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 boolean mGeckoFocused; // Used by Gecko thread
+ private boolean mIgnoreSelectionChange; // Used by Gecko thread
+
+ 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;
+ private static final int IME_RANGE_LINE_DASHED = 2;
+ private static final int IME_RANGE_LINE_SOLID = 3;
+ private static final int IME_RANGE_LINE_DOUBLE = 4;
+ private static final int IME_RANGE_LINE_WAVY = 5;
+
+ private static final int IME_RANGE_UNDERLINE = 1;
+ private static final int IME_RANGE_FORECOLOR = 2;
+ private static final int IME_RANGE_BACKCOLOR = 4;
+ private static final int IME_RANGE_LINECOLOR = 8;
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onKeyEvent(int action, int keyCode, int scanCode, int metaState,
+ long time, int unicodeChar, int baseUnicodeChar,
+ int domPrintableKeyValue, int repeatCount, int flags,
+ boolean isSynthesizedImeKey, KeyEvent event);
+
+ private void onKeyEvent(KeyEvent event, int action, int savedMetaState,
+ boolean isSynthesizedImeKey) {
+ // Use a separate action argument so we can override the key's original action,
+ // e.g. change ACTION_MULTIPLE to ACTION_DOWN. That way we don't have to allocate
+ // a new key event just to change its action field.
+ //
+ // Normally we expect event.getMetaState() to reflect the current meta-state; however,
+ // some software-generated key events may not have event.getMetaState() set, e.g. key
+ // events from Swype. Therefore, it's necessary to combine the key's meta-states
+ // with the meta-states that we keep separately in KeyListener
+ final int metaState = event.getMetaState() | savedMetaState;
+ final int unmodifiedMetaState = metaState &
+ ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK);
+ final int unicodeChar = event.getUnicodeChar(metaState);
+ final int domPrintableKeyValue =
+ unicodeChar >= ' ' ? unicodeChar :
+ unmodifiedMetaState != metaState ? event.getUnicodeChar(unmodifiedMetaState) :
+ 0;
+ onKeyEvent(action, event.getKeyCode(), event.getScanCode(),
+ metaState, event.getEventTime(), unicodeChar,
+ // e.g. for Ctrl+A, Android returns 0 for unicodeChar,
+ // but Gecko expects 'a', so we return that in baseUnicodeChar.
+ event.getUnicodeChar(0), domPrintableKeyValue, event.getRepeatCount(),
+ event.getFlags(), isSynthesizedImeKey, event);
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeSynchronize();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeReplaceText(int start, int end, String text);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeAddCompositionRange(int start, int end, int rangeType,
+ int rangeStyles, int rangeLineStyle,
+ boolean rangeBoldLine, int rangeForeColor,
+ int rangeBackColor, int rangeLineColor);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeUpdateComposition(int start, int end);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ private native void onImeRequestCursorUpdates(int requestMode);
+
+ /**
+ * Class that encapsulates asynchronous text editing. There are two copies of the
+ * text, a current copy and a shadow copy. Both can be modified independently through
+ * the current*** and shadow*** methods, respectively. The current copy can only be
+ * modified on the Gecko side and reflects the authoritative version of the text. The
+ * shadow copy can only be modified on the IC side and reflects what we think the
+ * current text is. Periodically, the shadow copy can be synced to the current copy
+ * through syncShadowText, so the shadow copy once again refers to the same text as
+ * the current copy.
+ */
+ private final class AsyncText {
+ // The current text is the update-to-date version of the text, and is only updated
+ // on the Gecko side.
+ private final SpannableStringBuilder mCurrentText = new SpannableStringBuilder();
+ // Track changes on the current side for syncing purposes.
+ // Start of the changed range in current text since last sync.
+ private int mCurrentStart = Integer.MAX_VALUE;
+ // End of the changed range (before the change) in current text since last sync.
+ private int mCurrentOldEnd;
+ // End of the changed range (after the change) in current text since last sync.
+ private int mCurrentNewEnd;
+ // Track selection changes separately.
+ private boolean mCurrentSelectionChanged;
+
+ // The shadow text is what we think the current text is on the Java side, and is
+ // periodically synced with the current text.
+ private final SpannableStringBuilder mShadowText = new SpannableStringBuilder();
+ // Track changes on the shadow side for syncing purposes.
+ // Start of the changed range in shadow text since last sync.
+ private int mShadowStart = Integer.MAX_VALUE;
+ // End of the changed range (before the change) in shadow text since last sync.
+ private int mShadowOldEnd;
+ // End of the changed range (after the change) in shadow text since last sync.
+ private int mShadowNewEnd;
+
+ private void addCurrentChangeLocked(final int start, final int oldEnd, final int newEnd) {
+ // Merge the new change into any existing change.
+ mCurrentStart = Math.min(mCurrentStart, start);
+ mCurrentOldEnd += Math.max(0, oldEnd - mCurrentNewEnd);
+ mCurrentNewEnd = newEnd + Math.max(0, mCurrentNewEnd - oldEnd);
+ }
+
+ public synchronized void currentReplace(final int start, final int end,
+ final CharSequence newText) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mCurrentText.replace(start, end, newText);
+ addCurrentChangeLocked(start, end, start + newText.length());
+ }
+
+ public synchronized void currentSetSelection(final int start, final int end) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ Selection.setSelection(mCurrentText, start, end);
+ mCurrentSelectionChanged = true;
+ }
+
+ public synchronized void currentSetSpan(final Object obj, final int start,
+ final int end, final int flags) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mCurrentText.setSpan(obj, start, end, flags);
+ addCurrentChangeLocked(start, end, end);
+ }
+
+ public synchronized void currentRemoveSpan(final Object obj) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ if (obj == null) {
+ mCurrentText.clearSpans();
+ addCurrentChangeLocked(0, mCurrentText.length(), mCurrentText.length());
+ return;
+ }
+ final int start = mCurrentText.getSpanStart(obj);
+ final int end = mCurrentText.getSpanEnd(obj);
+ if (start < 0 || end < 0) {
+ return;
+ }
+ mCurrentText.removeSpan(obj);
+ addCurrentChangeLocked(start, end, end);
+ }
+
+ // Return Spanned instead of Editable because the returned object is supposed to
+ // be read-only. Editing should be done through one of the current*** methods.
+ public Spanned getCurrentText() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ return mCurrentText;
+ }
+
+ private void addShadowChange(final int start, final int oldEnd, final int newEnd) {
+ // Merge the new change into any existing change.
+ mShadowStart = Math.min(mShadowStart, start);
+ mShadowOldEnd += Math.max(0, oldEnd - mShadowNewEnd);
+ mShadowNewEnd = newEnd + Math.max(0, mShadowNewEnd - oldEnd);
+ }
+
+ public void shadowReplace(final int start, final int end,
+ final CharSequence newText)
+ {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mShadowText.replace(start, end, newText);
+ addShadowChange(start, end, start + newText.length());
+ }
+
+ public void shadowSetSpan(final Object obj, final int start,
+ final int end, final int flags) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ mShadowText.setSpan(obj, start, end, flags);
+ addShadowChange(start, end, end);
+ }
+
+ public void shadowRemoveSpan(final Object obj) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ if (obj == null) {
+ mShadowText.clearSpans();
+ addShadowChange(0, mShadowText.length(), mShadowText.length());
+ return;
+ }
+ final int start = mShadowText.getSpanStart(obj);
+ final int end = mShadowText.getSpanEnd(obj);
+ if (start < 0 || end < 0) {
+ return;
+ }
+ mShadowText.removeSpan(obj);
+ addShadowChange(start, end, end);
+ }
+
+ // Return Spanned instead of Editable because the returned object is supposed to
+ // be read-only. Editing should be done through one of the shadow*** methods.
+ public Spanned getShadowText() {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ return mShadowText;
+ }
+
+ public synchronized void syncShadowText(final GeckoEditableListener listener) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+
+ if (mCurrentStart > mCurrentOldEnd && mShadowStart > mShadowOldEnd) {
+ // Still check selection changes.
+ if (!mCurrentSelectionChanged) {
+ return;
+ }
+ final int start = Selection.getSelectionStart(mCurrentText);
+ final int end = Selection.getSelectionEnd(mCurrentText);
+ Selection.setSelection(mShadowText, start, end);
+ mCurrentSelectionChanged = false;
+
+ if (listener != null) {
+ listener.onSelectionChange();
+ }
+ return;
+ }
+
+ // Copy the portion of the current text that has changed over to the shadow
+ // text, with consideration for any concurrent changes in the shadow text.
+ final int start = Math.min(mShadowStart, mCurrentStart);
+ final int shadowEnd = mShadowNewEnd + Math.max(0, mCurrentOldEnd - mShadowOldEnd);
+ final int currentEnd = mCurrentNewEnd + Math.max(0, mShadowOldEnd - mCurrentOldEnd);
+
+ // Perform replacement in two steps (delete and insert) so that old spans are
+ // properly deleted before identical new spans are inserted. Otherwise the new
+ // spans won't be inserted due to the text already having the old spans.
+ mShadowText.delete(start, shadowEnd);
+ mShadowText.insert(start, mCurrentText, start, currentEnd);
+
+ // SpannableStringBuilder has some internal logic to fix up selections, but we
+ // don't want that, so we always fix up the selection a second time.
+ final int selStart = Selection.getSelectionStart(mCurrentText);
+ final int selEnd = Selection.getSelectionEnd(mCurrentText);
+ Selection.setSelection(mShadowText, selStart, selEnd);
+
+ if (DEBUG && !mShadowText.equals(mCurrentText)) {
+ // Sanity check.
+ throw new IllegalStateException("Failed to sync: " +
+ mShadowStart + '-' + mShadowOldEnd + '-' + mShadowNewEnd + '/' +
+ mCurrentStart + '-' + mCurrentOldEnd + '-' + mCurrentNewEnd);
+ }
+
+ if (listener != null) {
+ // Call onTextChange after selection fix-up but before we call
+ // onSelectionChange.
+ listener.onTextChange();
+
+ if (mCurrentSelectionChanged || (mCurrentOldEnd != mCurrentNewEnd &&
+ (selStart >= mCurrentStart || selEnd >= mCurrentStart))) {
+ listener.onSelectionChange();
+ }
+ }
+
+ // These values ensure the first change is properly added.
+ mCurrentStart = mShadowStart = Integer.MAX_VALUE;
+ mCurrentOldEnd = mShadowOldEnd = 0;
+ mCurrentNewEnd = mShadowNewEnd = 0;
+ mCurrentSelectionChanged = false;
+ }
+ }
+
+ /* An action that alters the Editable
+
+ Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko
+ thread, the action stays on top of mActions queue. After the Gecko event is processed and
+ replied, the action is removed from the queue
+ */
+ private static final class Action {
+ // For input events (keypress, etc.); use with onImeSynchronize
+ static final int TYPE_EVENT = 0;
+ // For Editable.replace() call; use with onImeReplaceText
+ static final int TYPE_REPLACE_TEXT = 1;
+ // For Editable.setSpan() call; use with onImeSynchronize
+ static final int TYPE_SET_SPAN = 2;
+ // For Editable.removeSpan() call; use with onImeSynchronize
+ static final int TYPE_REMOVE_SPAN = 3;
+ // For switching handler; use with onImeSynchronize
+ static final int TYPE_SET_HANDLER = 4;
+
+ final int mType;
+ int mStart;
+ int mEnd;
+ CharSequence mSequence;
+ Object mSpanObject;
+ int mSpanFlags;
+ Handler mHandler;
+
+ Action(int type) {
+ mType = type;
+ }
+
+ static Action newReplaceText(CharSequence text, int start, int end) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid replace text offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid replace text offsets");
+ }
+
+ final Action action = new Action(TYPE_REPLACE_TEXT);
+ action.mSequence = text;
+ action.mStart = start;
+ action.mEnd = end;
+ return action;
+ }
+
+ static Action newSetSpan(Object object, int start, int end, int flags) {
+ if (start < 0 || start > end) {
+ Log.e(LOGTAG, "invalid span offsets: " + start + " to " + end);
+ throw new IllegalArgumentException("invalid span offsets");
+ }
+ final Action action = new Action(TYPE_SET_SPAN);
+ action.mSpanObject = object;
+ action.mStart = start;
+ action.mEnd = end;
+ action.mSpanFlags = flags;
+ return action;
+ }
+
+ static Action newRemoveSpan(Object object) {
+ final Action action = new Action(TYPE_REMOVE_SPAN);
+ action.mSpanObject = object;
+ return action;
+ }
+
+ static Action newSetHandler(Handler handler) {
+ final Action action = new Action(TYPE_SET_HANDLER);
+ action.mHandler = handler;
+ return action;
+ }
+ }
+
+ private void icOfferAction(final Action action) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "offer: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return;
+ }
+
+ mActions.offer(action);
+
+ switch (action.mType) {
+ case Action.TYPE_EVENT:
+ case Action.TYPE_SET_HANDLER:
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_SET_SPAN:
+ mText.shadowSetSpan(action.mSpanObject, action.mStart,
+ action.mEnd, action.mSpanFlags);
+ action.mSequence = TextUtils.substring(
+ mText.getShadowText(), action.mStart, action.mEnd);
+
+ mNeedUpdateComposition |= (action.mSpanFlags & Spanned.SPAN_INTERMEDIATE) == 0 &&
+ ((action.mSpanFlags & Spanned.SPAN_COMPOSING) != 0 ||
+ action.mSpanObject == Selection.SELECTION_START ||
+ action.mSpanObject == Selection.SELECTION_END);
+
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ final int flags = mText.getShadowText().getSpanFlags(action.mSpanObject);
+ mText.shadowRemoveSpan(action.mSpanObject);
+
+ mNeedUpdateComposition |= (flags & Spanned.SPAN_INTERMEDIATE) == 0 &&
+ (flags & Spanned.SPAN_COMPOSING) != 0;
+
+ onImeSynchronize();
+ break;
+
+ case Action.TYPE_REPLACE_TEXT:
+ // Always sync text after a replace action, so that if the Gecko
+ // text is not changed, we will revert the shadow text to before.
+ mNeedSync = true;
+
+ // Because we get composition styling here essentially for free,
+ // we don't need to check if we're in batch mode.
+ if (!icMaybeSendComposition(
+ action.mSequence, /* useEntireText */ true, /* notifyGecko */ false)) {
+ // Since we don't have a composition, we can try sending key events.
+ sendCharKeyEvents(action);
+ }
+ mText.shadowReplace(action.mStart, action.mEnd, action.mSequence);
+ onImeReplaceText(action.mStart, action.mEnd, action.mSequence.toString());
+ break;
+
+ default:
+ throw new IllegalStateException("Action not processed");
+ }
+ }
+
+ private KeyEvent [] synthesizeKeyEvents(CharSequence cs) {
+ try {
+ if (mKeyMap == null) {
+ mKeyMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+ }
+ } catch (Exception e) {
+ // KeyCharacterMap.UnavailableException is not found on Gingerbread;
+ // besides, it seems like HC and ICS will throw something other than
+ // KeyCharacterMap.UnavailableException; so use a generic Exception here
+ return null;
+ }
+ KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray());
+ if (keyEvents == null || keyEvents.length == 0) {
+ return null;
+ }
+ return keyEvents;
+ }
+
+ private void sendCharKeyEvents(Action action) {
+ if (action.mSequence.length() != 1 ||
+ (action.mSequence instanceof Spannable &&
+ ((Spannable)action.mSequence).nextSpanTransition(
+ -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) {
+ // Spans are not preserved when we use key events,
+ // so we need the sequence to not have any spans
+ return;
+ }
+ KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence);
+ if (keyEvents == null) {
+ return;
+ }
+ for (KeyEvent event : keyEvents) {
+ if (KeyEvent.isModifierKey(event.getKeyCode())) {
+ continue;
+ }
+ if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) {
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "sending: " + event);
+ }
+ onKeyEvent(event, event.getAction(),
+ /* metaState */ 0, /* isSynthesizedImeKey */ true);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ GeckoEditable(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ }
+
+ 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();
+
+ onViewChange(v);
+ }
+
+ @WrapForJNI(dispatchTo = "proxy") @Override
+ protected native void disposeNative();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onViewChange(final GeckoView v) {
+ if (DEBUG) {
+ // Called by nsWindow.
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onViewChange(" + v + ")");
+ }
+
+ final GeckoEditableListener newListener =
+ v != null ? GeckoInputConnection.create(v, this) : null;
+
+ final Runnable setListenerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set listener)");
+ }
+
+ mListener = newListener;
+
+ if (newListener == null) {
+ // We're being destroyed. By this point, we should have cleared all
+ // pending Runnables on the IC thread, so it's safe to call
+ // disposeNative here.
+ GeckoEditable.this.disposeNative();
+ }
+ }
+ };
+
+ // Post to UI thread first to make sure any code that is using the old input
+ // connection has finished running, before we switch to a new input connection or
+ // before we clear the input connection on destruction.
+ final Handler icHandler = mIcPostHandler;
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onViewChange (set IC)");
+ }
+
+ if (mView != null) {
+ // Detach the previous view.
+ mView.setInputConnectionListener(null);
+ }
+ if (v != null) {
+ // And attach the new view.
+ v.setInputConnectionListener((InputConnectionListener) newListener);
+ }
+
+ mView = v;
+ icHandler.post(setListenerRunnable);
+ }
+ });
+ }
+
+ private boolean onIcThread() {
+ return mIcRunHandler.getLooper() == Looper.myLooper();
+ }
+
+ private void assertOnIcThread() {
+ ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW);
+ }
+
+ private void geckoPostToIc(Runnable runnable) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ mIcPostHandler.post(runnable);
+ }
+
+ private Object getField(Object obj, String field, Object def) {
+ try {
+ return obj.getClass().getField(field).get(obj);
+ } catch (Exception e) {
+ return def;
+ }
+ }
+
+ /**
+ * Send composition ranges to Gecko for the entire shadow text.
+ */
+ private void icMaybeSendComposition() {
+ if (!mNeedUpdateComposition) {
+ return;
+ }
+
+ icMaybeSendComposition(mText.getShadowText(),
+ /* useEntireText */ false, /* notifyGecko */ true);
+ }
+
+ /**
+ * Send composition ranges to Gecko if the text has composing spans.
+ *
+ * @param sequence Text with possible composing spans
+ * @param useEntireText If text has composing spans, treat the entire text as
+ * a Gecko composition, instead of just the spanned part.
+ * @param notifyGecko Notify Gecko of the new composition ranges;
+ * otherwise, the caller is responsible for notifying Gecko.
+ * @return Whether there was a composition
+ */
+ private boolean icMaybeSendComposition(final CharSequence sequence,
+ final boolean useEntireText,
+ final boolean notifyGecko) {
+ mNeedUpdateComposition = false;
+
+ int selStart = Selection.getSelectionStart(sequence);
+ int selEnd = Selection.getSelectionEnd(sequence);
+
+ if (sequence instanceof Spanned) {
+ final Spanned text = (Spanned) sequence;
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ boolean found = false;
+ int composingStart = useEntireText ? 0 : Integer.MAX_VALUE;
+ int composingEnd = useEntireText ? text.length() : 0;
+
+ // Find existence and range of any composing spans (spans with the
+ // SPAN_COMPOSING flag set).
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) == 0) {
+ continue;
+ }
+ found = true;
+ if (useEntireText) {
+ break;
+ }
+ composingStart = Math.min(composingStart, text.getSpanStart(span));
+ composingEnd = Math.max(composingEnd, text.getSpanEnd(span));
+ }
+
+ if (useEntireText && (selStart < 0 || selEnd < 0)) {
+ selStart = composingEnd;
+ selEnd = composingEnd;
+ }
+
+ if (found) {
+ icSendComposition(text, selStart, selEnd, composingStart, composingEnd);
+ if (notifyGecko) {
+ onImeUpdateComposition(composingStart, composingEnd);
+ }
+ return true;
+ }
+ }
+
+ if (notifyGecko) {
+ // Set the selection by using a composition without ranges
+ onImeUpdateComposition(selStart, selEnd);
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "icSendComposition(): no composition");
+ }
+ return false;
+ }
+
+ private void icSendComposition(final Spanned text,
+ final int selStart, final int selEnd,
+ final int composingStart, final int composingEnd) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "icSendComposition(\"" + text + "\", " +
+ composingStart + ", " + composingEnd + ")");
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd);
+ Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd);
+ }
+
+ if (selEnd >= composingStart && selEnd <= composingEnd) {
+ onImeAddCompositionRange(
+ selEnd - composingStart, selEnd - composingStart,
+ IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0);
+ }
+
+ int rangeStart = composingStart;
+ TextPaint tp = new TextPaint();
+ TextPaint emptyTp = new TextPaint();
+ // set initial foreground color to 0, because we check for tp.getColor() == 0
+ // below to decide whether to pass a foreground color to Gecko
+ emptyTp.setColor(0);
+ do {
+ int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE;
+ boolean rangeBoldLine = false;
+ int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0;
+ int rangeEnd = text.nextSpanTransition(rangeStart, composingEnd, Object.class);
+
+ if (selStart > rangeStart && selStart < rangeEnd) {
+ rangeEnd = selStart;
+ } else if (selEnd > rangeStart && selEnd < rangeEnd) {
+ rangeEnd = selEnd;
+ }
+ CharacterStyle[] styleSpans =
+ text.getSpans(rangeStart, rangeEnd, CharacterStyle.class);
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " +
+ rangeStart + "-" + rangeEnd);
+ }
+
+ if (styleSpans.length == 0) {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDRAWTEXT
+ : IME_RANGE_RAWINPUT;
+ } else {
+ rangeType = (selStart == rangeStart && selEnd == rangeEnd)
+ ? IME_RANGE_SELECTEDCONVERTEDTEXT
+ : IME_RANGE_CONVERTEDTEXT;
+ tp.set(emptyTp);
+ for (CharacterStyle span : styleSpans) {
+ span.updateDrawState(tp);
+ }
+ int tpUnderlineColor = 0;
+ float tpUnderlineThickness = 0.0f;
+
+ // These TextPaint fields only exist on Android ICS+ and are not in the SDK.
+ tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0);
+ tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f);
+ if (tpUnderlineColor != 0) {
+ rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR;
+ rangeLineColor = tpUnderlineColor;
+ // Approximately translate underline thickness to what Gecko understands
+ if (tpUnderlineThickness <= 0.5f) {
+ rangeLineStyle = IME_RANGE_LINE_DOTTED;
+ } else {
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ if (tpUnderlineThickness >= 2.0f) {
+ rangeBoldLine = true;
+ }
+ }
+ } else if (tp.isUnderlineText()) {
+ rangeStyles |= IME_RANGE_UNDERLINE;
+ rangeLineStyle = IME_RANGE_LINE_SOLID;
+ }
+ if (tp.getColor() != 0) {
+ rangeStyles |= IME_RANGE_FORECOLOR;
+ rangeForeColor = tp.getColor();
+ }
+ if (tp.bgColor != 0) {
+ rangeStyles |= IME_RANGE_BACKCOLOR;
+ rangeBackColor = tp.bgColor;
+ }
+ }
+ onImeAddCompositionRange(
+ rangeStart - composingStart, rangeEnd - composingStart,
+ rangeType, rangeStyles, rangeLineStyle, rangeBoldLine,
+ rangeForeColor, rangeBackColor, rangeLineColor);
+ rangeStart = rangeEnd;
+
+ if (DEBUG) {
+ Log.d(LOGTAG, " added " + rangeType +
+ " : " + Integer.toHexString(rangeStyles) +
+ " : " + Integer.toHexString(rangeForeColor) +
+ " : " + Integer.toHexString(rangeBackColor));
+ }
+ } while (rangeStart < composingEnd);
+ }
+
+ // GeckoEditableClient interface
+
+ @Override
+ public void sendKeyEvent(final KeyEvent event, int action, int metaState) {
+ if (DEBUG) {
+ assertOnIcThread();
+ Log.d(LOGTAG, "sendKeyEvent(" + event + ", " + action + ", " + metaState + ")");
+ }
+ /*
+ We are actually sending two events to Gecko here,
+ 1. Event from the event parameter (key event)
+ 2. Sync event from the icOfferAction call
+ The first event is a normal event that does not reply back to us,
+ the second sync event will have a reply, during which we see that there is a pending
+ event-type action, and update the shadow text accordingly.
+ */
+ icMaybeSendComposition();
+ onKeyEvent(event, action, metaState, /* isSynthesizedImeKey */ false);
+ icOfferAction(new Action(Action.TYPE_EVENT));
+ }
+
+ @Override
+ public Editable getEditable() {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "getEditable() called on non-IC thread");
+ }
+ return null;
+ }
+ if (mListener == null) {
+ // We haven't initialized or we've been destroyed.
+ return null;
+ }
+ return mProxy;
+ }
+
+ @Override
+ public void setBatchMode(boolean inBatchMode) {
+ if (!onIcThread()) {
+ // Android may be holding an old InputConnection; ignore
+ if (DEBUG) {
+ Log.i(LOGTAG, "setBatchMode() called on non-IC thread");
+ }
+ return;
+ }
+
+ mInBatchMode = inBatchMode;
+
+ if (!inBatchMode && mNeedSync) {
+ icSyncShadowText();
+ }
+ }
+
+ /* package */ void icSyncShadowText() {
+ if (mListener == null) {
+ // Not yet attached or already destroyed.
+ return;
+ }
+
+ if (mInBatchMode || !mActions.isEmpty()) {
+ mNeedSync = true;
+ return;
+ }
+
+ mNeedSync = false;
+ mText.syncShadowText(mListener);
+ }
+
+ private void geckoScheduleSyncShadowText() {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ icSyncShadowText();
+ }
+ });
+ }
+
+ @Override
+ public void setSuppressKeyUp(boolean suppress) {
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+ // Suppress key up event generated as a result of
+ // translating characters to key events
+ mSuppressKeyUp = suppress;
+ }
+
+ @Override // GeckoEditableClient
+ public Handler setInputConnectionHandler(final Handler handler) {
+ if (handler == mIcRunHandler) {
+ return mIcRunHandler;
+ }
+ if (DEBUG) {
+ assertOnIcThread();
+ }
+
+ // There are three threads at this point: Gecko thread, old IC thread, and new IC
+ // thread, and we want to safely switch from old IC thread to new IC thread.
+ // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that
+ // the Gecko thread is stopped at a known point. At the same time, the old IC
+ // thread blocks on the action; this ensures that the old IC thread is stopped at
+ // a known point. Finally, inside the Gecko thread, we post a Runnable to the old
+ // IC thread; this Runnable switches from old IC thread to new IC thread. We
+ // switch IC thread on the old IC thread to ensure any pending Runnables on the
+ // old IC thread are processed before we switch over. Inside the Gecko thread, we
+ // also post a Runnable to the new IC thread; this Runnable blocks until the
+ // switch is complete; this ensures that the new IC thread won't accept
+ // InputConnection calls until after the switch.
+
+ handler.post(new Runnable() { // Make the new IC thread wait.
+ @Override
+ public void run() {
+ synchronized (handler) {
+ while (mIcRunHandler != handler) {
+ try {
+ handler.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ }
+ }
+ });
+
+ icOfferAction(Action.newSetHandler(handler));
+ return handler;
+ }
+
+ @Override // GeckoEditableClient
+ public void postToInputConnection(final Runnable runnable) {
+ mIcPostHandler.post(runnable);
+ }
+
+ @Override // GeckoEditableClient
+ public void requestCursorUpdates(int requestMode) {
+ onImeRequestCursorUpdates(requestMode);
+ }
+
+ private void geckoSetIcHandler(final Handler newHandler) {
+ geckoPostToIc(new Runnable() { // posting to old IC thread
+ @Override
+ public void run() {
+ synchronized (newHandler) {
+ mIcRunHandler = newHandler;
+ newHandler.notify();
+ }
+ }
+ });
+
+ // At this point, all future Runnables should be posted to the new IC thread, but
+ // we don't switch mIcRunHandler yet because there may be pending Runnables on the
+ // old IC thread still waiting to run.
+ mIcPostHandler = newHandler;
+ }
+
+ private void geckoActionReply(final Action action) {
+ if (!mGeckoFocused) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale reply");
+ }
+ return;
+ }
+
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "reply: Action(" +
+ getConstantName(Action.class, "TYPE_", action.mType) + ")");
+ }
+ switch (action.mType) {
+ case Action.TYPE_SET_SPAN:
+ final int len = mText.getCurrentText().length();
+ if (action.mStart > len || action.mEnd > len ||
+ !TextUtils.substring(mText.getCurrentText(), action.mStart,
+ action.mEnd).equals(action.mSequence)) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "discarding stale set span call");
+ }
+ break;
+ }
+ mText.currentSetSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags);
+ break;
+
+ case Action.TYPE_REMOVE_SPAN:
+ mText.currentRemoveSpan(action.mSpanObject);
+ break;
+
+ case Action.TYPE_SET_HANDLER:
+ geckoSetIcHandler(action.mHandler);
+ break;
+ }
+ }
+
+ private void notifyCommitComposition() {
+ // 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.
+ }
+
+ private void notifyCancelComposition() {
+ // Composition should have been canceled on our side
+ // through text update notifications; verify that here.
+ if (DEBUG) {
+ final Spanned text = mText.getCurrentText();
+ final Object[] spans = text.getSpans(0, text.length(), Object.class);
+ for (Object span : spans) {
+ if ((text.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
+ throw new IllegalStateException("composition not cancelled");
+ }
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIME(final int type) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply()
+ if (type != GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ Log.d(LOGTAG, "notifyIME(" +
+ getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) +
+ ")");
+ }
+ }
+
+ if (type == GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ geckoActionReply(mActions.poll());
+ if (!mGeckoFocused || !mActions.isEmpty()) {
+ // Only post to IC thread below when the queue is empty.
+ return;
+ }
+ } else if (type == GeckoEditableListener.NOTIFY_IME_TO_COMMIT_COMPOSITION) {
+ notifyCommitComposition();
+ return;
+ } else if (type == GeckoEditableListener.NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+ notifyCancelComposition();
+ return;
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (type == GeckoEditableListener.NOTIFY_IME_REPLY_EVENT) {
+ if (mNeedSync) {
+ icSyncShadowText();
+ }
+ return;
+ }
+
+ if (type == GeckoEditableListener.NOTIFY_IME_OF_FOCUS && mListener != null) {
+ mNeedSync = false;
+ mText.syncShadowText(/* listener */ null);
+ }
+
+ if (mListener != null) {
+ mListener.notifyIME(type);
+ }
+ }
+ });
+
+ // Update the mGeckoFocused flag.
+ if (type == GeckoEditableListener.NOTIFY_IME_OF_BLUR) {
+ mGeckoFocused = false;
+ } else if (type == GeckoEditableListener.NOTIFY_IME_OF_FOCUS) {
+ mGeckoFocused = true;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIMEContext(final int state, final String typeHint,
+ final String modeHint, final String actionHint) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "notifyIMEContext(" +
+ getConstantName(GeckoEditableListener.class, "IME_STATE_", state) +
+ ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.notifyIMEContext(state, typeHint, modeHint, actionHint);
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onSelectionChange(final int start, final int end) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")");
+ }
+
+ final int currentLength = mText.getCurrentText().length();
+ if (start < 0 || start > currentLength || end < 0 || end > currentLength) {
+ Log.e(LOGTAG, "invalid selection notification range: " +
+ start + " to " + end + ", length: " + currentLength);
+ throw new IllegalArgumentException("invalid selection notification range");
+ }
+
+ if (mIgnoreSelectionChange) {
+ mIgnoreSelectionChange = false;
+ } else {
+ mText.currentSetSelection(start, end);
+ }
+
+ geckoScheduleSyncShadowText();
+ }
+
+ private boolean geckoIsSameText(int start, int oldEnd, CharSequence newText) {
+ return oldEnd - start == newText.length() &&
+ TextUtils.regionMatches(mText.getCurrentText(), start, newText, 0, oldEnd - start);
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onTextChange(final CharSequence text, final int start,
+ final int unboundedOldEnd, final int unboundedNewEnd) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onTextChange(");
+ debugAppend(sb, text);
+ sb.append(", ").append(start).append(", ")
+ .append(unboundedOldEnd).append(", ")
+ .append(unboundedNewEnd).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (start < 0 || start > unboundedOldEnd) {
+ Log.e(LOGTAG, "invalid text notification range: " +
+ start + " to " + unboundedOldEnd);
+ throw new IllegalArgumentException("invalid text notification range");
+ }
+
+ final int currentLength = mText.getCurrentText().length();
+
+ /* For the "end" parameters, Gecko can pass in a large
+ number to denote "end of the text". Fix that here */
+ final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd;
+ // new end should always match text
+ if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) {
+ Log.e(LOGTAG, "newEnd does not match text: " + unboundedNewEnd + " vs " +
+ (start + text.length()));
+ throw new IllegalArgumentException("newEnd does not match text");
+ }
+
+ final int newEnd = start + text.length();
+ final Action action = mActions.peek();
+
+ if (start == 0 && unboundedOldEnd > currentLength) {
+ // Simply replace the text for newly-focused editors. Replace in two steps to
+ // properly clear composing spans that span the whole range.
+ mText.currentReplace(0, currentLength, "");
+ mText.currentReplace(0, 0, text);
+
+ // Don't ignore the next selection change because we are re-syncing with Gecko
+ mIgnoreSelectionChange = false;
+
+ } else if (action != null &&
+ action.mType == Action.TYPE_REPLACE_TEXT &&
+ start <= action.mStart &&
+ oldEnd >= action.mEnd &&
+ newEnd >= action.mStart + action.mSequence.length()) {
+
+ // Try to preserve both old spans and new spans in action.mSequence.
+ // indexInText is where we can find waction.mSequence within the passed in text.
+ final int startWithinText = action.mStart - start;
+ int indexInText = TextUtils.indexOf(text, action.mSequence, startWithinText);
+ if (indexInText < 0 && startWithinText >= action.mSequence.length()) {
+ indexInText = text.toString().lastIndexOf(action.mSequence.toString(),
+ startWithinText);
+ }
+
+ if (indexInText < 0) {
+ // Text was changed from under us. We are forced to discard any new spans.
+ mText.currentReplace(start, oldEnd, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+
+ } else if (indexInText == 0 && text.length() == action.mSequence.length() &&
+ oldEnd - start == action.mEnd - action.mStart) {
+ // The new change exactly matches our saved change, so do a direct replace.
+ mText.currentReplace(start, oldEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+
+ } else {
+ // The sequence is embedded within the changed text, so we have to perform
+ // replacement in parts. First replace part of text before the sequence.
+ mText.currentReplace(start, action.mStart, text.subSequence(0, indexInText));
+
+ // Then replace part of the text after the sequence.
+ final int actionStart = indexInText + start;
+ final int delta = actionStart - action.mStart;
+ final int actionEnd = delta + action.mEnd;
+
+ final Spanned currentText = mText.getCurrentText();
+ final boolean resetSelStart = Selection.getSelectionStart(currentText) == actionEnd;
+ final boolean resetSelEnd = Selection.getSelectionEnd(currentText) == actionEnd;
+
+ mText.currentReplace(actionEnd, delta + oldEnd, text.subSequence(
+ indexInText + action.mSequence.length(), text.length()));
+
+ // The replacement above may have shifted our selection, if the selection
+ // was at the start of the replacement range. If so, we need to reset
+ // our selection to the previous position.
+ if (resetSelStart || resetSelEnd) {
+ mText.currentSetSelection(
+ resetSelStart ? actionEnd : Selection.getSelectionStart(currentText),
+ resetSelEnd ? actionEnd : Selection.getSelectionEnd(currentText));
+ }
+
+ // Finally replace the sequence itself to preserve new spans.
+ mText.currentReplace(actionStart, actionEnd, action.mSequence);
+
+ // Ignore the next selection change because the selection change is a
+ // side-effect of the replace-text event we sent.
+ mIgnoreSelectionChange = true;
+ }
+
+ } else if (geckoIsSameText(start, oldEnd, text)) {
+ // Nothing to do because the text is the same. This could happen when
+ // the composition is updated for example, in which case we want to keep the
+ // Java selection.
+ mIgnoreSelectionChange = mIgnoreSelectionChange ||
+ (action != null && action.mType == Action.TYPE_REPLACE_TEXT);
+ return;
+
+ } else {
+ // Gecko side initiated the text change. Replace in two steps to properly
+ // clear composing spans that span the whole range.
+ mText.currentReplace(start, oldEnd, "");
+ mText.currentReplace(start, start, text);
+
+ // Don't ignore the next selection change because we are forced to re-sync
+ // with Gecko here.
+ mIgnoreSelectionChange = false;
+ }
+
+ // onTextChange is always followed by onSelectionChange, so we let
+ // onSelectionChange schedule a shadow text sync.
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onDefaultKeyEvent(final KeyEvent event) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
+ sb.append("action=").append(event.getAction()).append(", ")
+ .append("keyCode=").append(event.getKeyCode()).append(", ")
+ .append("metaState=").append(event.getMetaState()).append(", ")
+ .append("time=").append(event.getEventTime()).append(", ")
+ .append("repeatCount=").append(event.getRepeatCount()).append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onDefaultKeyEvent(event);
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void updateCompositionRects(final RectF[] aRects) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "updateCompositionRects(aRects.length = " + aRects.length + ")");
+ }
+ geckoPostToIc(new Runnable() {
+ @Override
+ public void run() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.updateCompositionRects(aRects);
+ }
+ });
+ }
+
+ // InvocationHandler interface
+
+ static String getConstantName(Class<?> cls, String prefix, Object value) {
+ for (Field fld : cls.getDeclaredFields()) {
+ try {
+ if (fld.getName().startsWith(prefix) &&
+ fld.get(null).equals(value)) {
+ return fld.getName();
+ }
+ } catch (IllegalAccessException e) {
+ }
+ }
+ return String.valueOf(value);
+ }
+
+ static StringBuilder debugAppend(StringBuilder sb, Object obj) {
+ if (obj == null) {
+ sb.append("null");
+ } else if (obj instanceof GeckoEditable) {
+ sb.append("GeckoEditable");
+ } else if (Proxy.isProxyClass(obj.getClass())) {
+ debugAppend(sb, Proxy.getInvocationHandler(obj));
+ } else if (obj instanceof CharSequence) {
+ sb.append('"').append(obj.toString().replace('\n', '\u21b2')).append('"');
+ } else if (obj.getClass().isArray()) {
+ sb.append(obj.getClass().getComponentType().getSimpleName()).append('[')
+ .append(Array.getLength(obj)).append(']');
+ } else {
+ sb.append(obj);
+ }
+ return sb;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ Object target;
+ final Class<?> methodInterface = method.getDeclaringClass();
+ if (DEBUG) {
+ // Editable methods should all be called from the IC thread
+ assertOnIcThread();
+ }
+ if (methodInterface == Editable.class ||
+ methodInterface == Appendable.class ||
+ methodInterface == Spannable.class) {
+ // Method alters the Editable; route calls to our implementation
+ target = this;
+ } else {
+ target = mText.getShadowText();
+ }
+ Object ret;
+ try {
+ ret = method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ // Bug 817386
+ // Most likely Gecko has changed the text while GeckoInputConnection is
+ // trying to access the text. If we pass through the exception here, Fennec
+ // will crash due to a lack of exception handler. Log the exception and
+ // return an empty value instead.
+ if (!(e.getCause() instanceof IndexOutOfBoundsException)) {
+ // Only handle IndexOutOfBoundsException for now,
+ // as other exceptions might signal other bugs
+ throw e;
+ }
+ Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause());
+ Class<?> retClass = method.getReturnType();
+ if (retClass == Character.TYPE) {
+ ret = '\0';
+ } else if (retClass == Integer.TYPE) {
+ ret = 0;
+ } else if (retClass == String.class) {
+ ret = "";
+ } else {
+ ret = null;
+ }
+ }
+ if (DEBUG) {
+ StringBuilder log = new StringBuilder(method.getName());
+ log.append("(");
+ if (args != null) {
+ for (Object arg : args) {
+ debugAppend(log, arg).append(", ");
+ }
+ if (args.length > 0) {
+ log.setLength(log.length() - 2);
+ }
+ }
+ if (method.getReturnType().equals(Void.TYPE)) {
+ log.append(")");
+ } else {
+ debugAppend(log.append(") = "), ret);
+ }
+ Log.d(LOGTAG, log.toString());
+ }
+ return ret;
+ }
+
+ // Spannable interface
+
+ @Override
+ public void removeSpan(Object what) {
+ if (what == null) {
+ return;
+ }
+
+ if (what == Selection.SELECTION_START ||
+ what == Selection.SELECTION_END) {
+ Log.w(LOGTAG, "selection removed with removeSpan()");
+ }
+
+ icOfferAction(Action.newRemoveSpan(what));
+ }
+
+ @Override
+ public void setSpan(Object what, int start, int end, int flags) {
+ icOfferAction(Action.newSetSpan(what, start, end, flags));
+ }
+
+ // Appendable interface
+
+ @Override
+ public Editable append(CharSequence text) {
+ return replace(mProxy.length(), mProxy.length(), text, 0, text.length());
+ }
+
+ @Override
+ public Editable append(CharSequence text, int start, int end) {
+ return replace(mProxy.length(), mProxy.length(), text, start, end);
+ }
+
+ @Override
+ public Editable append(char text) {
+ return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1);
+ }
+
+ // Editable interface
+
+ @Override
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ @Override
+ public void setFilters(InputFilter[] filters) {
+ mFilters = filters;
+ }
+
+ @Override
+ public void clearSpans() {
+ /* XXX this clears the selection spans too,
+ but there is no way to clear the corresponding selection in Gecko */
+ Log.w(LOGTAG, "selection cleared with clearSpans()");
+ icOfferAction(Action.newRemoveSpan(/* what */ null));
+ }
+
+ @Override
+ public Editable replace(int st, int en,
+ CharSequence source, int start, int end) {
+
+ CharSequence text = source;
+ if (start < 0 || start > end || end > text.length()) {
+ Log.e(LOGTAG, "invalid replace offsets: " +
+ start + " to " + end + ", length: " + text.length());
+ throw new IllegalArgumentException("invalid replace offsets");
+ }
+ if (start != 0 || end != text.length()) {
+ text = text.subSequence(start, end);
+ }
+ if (mFilters != null) {
+ // Filter text before sending the request to Gecko
+ for (int i = 0; i < mFilters.length; ++i) {
+ final CharSequence cs = mFilters[i].filter(
+ text, 0, text.length(), mProxy, st, en);
+ if (cs != null) {
+ text = cs;
+ }
+ }
+ }
+ if (text == source) {
+ // Always create a copy
+ text = new SpannableString(source);
+ }
+ icOfferAction(Action.newReplaceText(text, Math.min(st, en), Math.max(st, en)));
+ return mProxy;
+ }
+
+ @Override
+ public void clear() {
+ replace(0, mProxy.length(), "", 0, 0);
+ }
+
+ @Override
+ public Editable delete(int st, int en) {
+ return replace(st, en, "", 0, 0);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text,
+ int start, int end) {
+ return replace(where, where, text, start, end);
+ }
+
+ @Override
+ public Editable insert(int where, CharSequence text) {
+ return replace(where, where, text, 0, text.length());
+ }
+
+ @Override
+ public Editable replace(int st, int en, CharSequence text) {
+ return replace(st, en, text, 0, text.length());
+ }
+
+ /* GetChars interface */
+
+ @Override
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ /* overridden Editable interface methods in GeckoEditable must not be called directly
+ outside of GeckoEditable. Instead, the call must go through mProxy, which ensures
+ that Java is properly synchronized with Gecko */
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* Spanned interface */
+
+ @Override
+ public int getSpanEnd(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanFlags(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int getSpanStart(Object tag) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration
+ public int nextSpanTransition(int start, int limit, Class type) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ /* CharSequence interface */
+
+ @Override
+ public char charAt(int index) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public int length() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException("method must be called through mProxy");
+ }
+}
+