michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.gfx.InputConnectionHandler; michael@0: import org.mozilla.gecko.gfx.LayerView; michael@0: import org.mozilla.gecko.util.GeckoEventListener; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; michael@0: michael@0: import org.json.JSONObject; michael@0: michael@0: import android.os.Build; michael@0: import android.os.Handler; michael@0: import android.os.Looper; michael@0: import android.text.Editable; michael@0: import android.text.InputFilter; michael@0: import android.text.Selection; michael@0: import android.text.Spannable; michael@0: import android.text.SpannableString; michael@0: import android.text.SpannableStringBuilder; michael@0: import android.text.Spanned; michael@0: import android.text.TextPaint; michael@0: import android.text.TextUtils; michael@0: import android.text.style.CharacterStyle; michael@0: import android.util.Log; michael@0: import android.view.KeyCharacterMap; michael@0: import android.view.KeyEvent; michael@0: michael@0: import java.lang.reflect.Field; michael@0: import java.lang.reflect.InvocationHandler; michael@0: import java.lang.reflect.InvocationTargetException; michael@0: import java.lang.reflect.Method; michael@0: import java.lang.reflect.Proxy; michael@0: import java.util.concurrent.ConcurrentLinkedQueue; michael@0: import java.util.concurrent.Semaphore; michael@0: michael@0: // interface for the IC thread michael@0: interface GeckoEditableClient { michael@0: void sendEvent(GeckoEvent event); michael@0: Editable getEditable(); michael@0: void setUpdateGecko(boolean update); michael@0: void setSuppressKeyUp(boolean suppress); michael@0: Handler getInputConnectionHandler(); michael@0: boolean setInputConnectionHandler(Handler handler); michael@0: } michael@0: michael@0: /* interface for the Editable to listen to the Gecko thread michael@0: and also for the IC thread to listen to the Editable */ michael@0: interface GeckoEditableListener { michael@0: // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko michael@0: final int NOTIFY_IME_OPEN_VKB = -2; michael@0: final int NOTIFY_IME_REPLY_EVENT = -1; michael@0: final int NOTIFY_IME_OF_FOCUS = 1; michael@0: final int NOTIFY_IME_OF_BLUR = 2; michael@0: final int NOTIFY_IME_TO_COMMIT_COMPOSITION = 7; michael@0: final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 8; michael@0: // IME enabled state for notifyIMEContext() michael@0: final int IME_STATE_DISABLED = 0; michael@0: final int IME_STATE_ENABLED = 1; michael@0: final int IME_STATE_PASSWORD = 2; michael@0: final int IME_STATE_PLUGIN = 3; michael@0: michael@0: void notifyIME(int type); michael@0: void notifyIMEContext(int state, String typeHint, michael@0: String modeHint, String actionHint); michael@0: void onSelectionChange(int start, int end); michael@0: void onTextChange(String text, int start, int oldEnd, int newEnd); michael@0: } michael@0: michael@0: /* michael@0: GeckoEditable implements only some functions of Editable michael@0: The field mText contains the actual underlying michael@0: SpannableStringBuilder/Editable that contains our text. michael@0: */ michael@0: final class GeckoEditable michael@0: implements InvocationHandler, Editable, michael@0: GeckoEditableClient, GeckoEditableListener, GeckoEventListener { michael@0: michael@0: private static final boolean DEBUG = false; michael@0: private static final String LOGTAG = "GeckoEditable"; michael@0: michael@0: // Filters to implement Editable's filtering functionality michael@0: private InputFilter[] mFilters; michael@0: michael@0: private final SpannableStringBuilder mText; michael@0: private final SpannableStringBuilder mChangedText; michael@0: private final Editable mProxy; michael@0: private final ActionQueue mActionQueue; michael@0: michael@0: // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables michael@0: // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to michael@0: // The two can be different when switching from one handler to another michael@0: private Handler mIcRunHandler; michael@0: private Handler mIcPostHandler; michael@0: michael@0: private GeckoEditableListener mListener; michael@0: private int mSavedSelectionStart; michael@0: private volatile int mGeckoUpdateSeqno; michael@0: private int mIcUpdateSeqno; michael@0: private int mLastIcUpdateSeqno; michael@0: private boolean mUpdateGecko; michael@0: private boolean mFocused; // Used by IC thread michael@0: private boolean mGeckoFocused; // Used by Gecko thread michael@0: private volatile boolean mSuppressCompositions; michael@0: private volatile boolean mSuppressKeyUp; michael@0: michael@0: /* An action that alters the Editable michael@0: michael@0: Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko michael@0: thread, the action stays on top of mActions queue. After the Gecko event is processed and michael@0: replied, the action is removed from the queue michael@0: */ michael@0: private static final class Action { michael@0: // For input events (keypress, etc.); use with IME_SYNCHRONIZE michael@0: static final int TYPE_EVENT = 0; michael@0: // For Editable.replace() call; use with IME_REPLACE_TEXT michael@0: static final int TYPE_REPLACE_TEXT = 1; michael@0: /* For Editable.setSpan(Selection...) call; use with IME_SYNCHRONIZE michael@0: Note that we don't use this with IME_SET_SELECTION because we don't want to update the michael@0: Gecko selection at the point of this action. The Gecko selection is updated only after michael@0: IC has updated its selection (during IME_SYNCHRONIZE reply) */ michael@0: static final int TYPE_SET_SELECTION = 2; michael@0: // For Editable.setSpan() call; use with IME_SYNCHRONIZE michael@0: static final int TYPE_SET_SPAN = 3; michael@0: // For Editable.removeSpan() call; use with IME_SYNCHRONIZE michael@0: static final int TYPE_REMOVE_SPAN = 4; michael@0: // For focus events (in notifyIME); use with IME_ACKNOWLEDGE_FOCUS michael@0: static final int TYPE_ACKNOWLEDGE_FOCUS = 5; michael@0: // For switching handler; use with IME_SYNCHRONIZE michael@0: static final int TYPE_SET_HANDLER = 6; michael@0: michael@0: final int mType; michael@0: int mStart; michael@0: int mEnd; michael@0: CharSequence mSequence; michael@0: Object mSpanObject; michael@0: int mSpanFlags; michael@0: boolean mShouldUpdate; michael@0: Handler mHandler; michael@0: michael@0: Action(int type) { michael@0: mType = type; michael@0: } michael@0: michael@0: static Action newReplaceText(CharSequence text, int start, int end) { michael@0: if (start < 0 || start > end) { michael@0: throw new IllegalArgumentException( michael@0: "invalid replace text offsets: " + start + " to " + end); michael@0: } michael@0: final Action action = new Action(TYPE_REPLACE_TEXT); michael@0: action.mSequence = text; michael@0: action.mStart = start; michael@0: action.mEnd = end; michael@0: return action; michael@0: } michael@0: michael@0: static Action newSetSelection(int start, int end) { michael@0: // start == -1 when the start offset should remain the same michael@0: // end == -1 when the end offset should remain the same michael@0: if (start < -1 || end < -1) { michael@0: throw new IllegalArgumentException( michael@0: "invalid selection offsets: " + start + " to " + end); michael@0: } michael@0: final Action action = new Action(TYPE_SET_SELECTION); michael@0: action.mStart = start; michael@0: action.mEnd = end; michael@0: return action; michael@0: } michael@0: michael@0: static Action newSetSpan(Object object, int start, int end, int flags) { michael@0: if (start < 0 || start > end) { michael@0: throw new IllegalArgumentException( michael@0: "invalid span offsets: " + start + " to " + end); michael@0: } michael@0: final Action action = new Action(TYPE_SET_SPAN); michael@0: action.mSpanObject = object; michael@0: action.mStart = start; michael@0: action.mEnd = end; michael@0: action.mSpanFlags = flags; michael@0: return action; michael@0: } michael@0: michael@0: static Action newSetHandler(Handler handler) { michael@0: final Action action = new Action(TYPE_SET_HANDLER); michael@0: action.mHandler = handler; michael@0: return action; michael@0: } michael@0: } michael@0: michael@0: /* Queue of editing actions sent to Gecko thread that michael@0: the Gecko thread has not responded to yet */ michael@0: private final class ActionQueue { michael@0: private final ConcurrentLinkedQueue mActions; michael@0: private final Semaphore mActionsActive; michael@0: private KeyCharacterMap mKeyMap; michael@0: michael@0: ActionQueue() { michael@0: mActions = new ConcurrentLinkedQueue(); michael@0: mActionsActive = new Semaphore(1); michael@0: } michael@0: michael@0: void offer(Action action) { michael@0: if (DEBUG) { michael@0: assertOnIcThread(); michael@0: Log.d(LOGTAG, "offer: Action(" + michael@0: getConstantName(Action.class, "TYPE_", action.mType) + ")"); michael@0: } michael@0: /* Events don't need update because they generate text/selection michael@0: notifications which will do the updating for us */ michael@0: if (action.mType != Action.TYPE_EVENT && michael@0: action.mType != Action.TYPE_ACKNOWLEDGE_FOCUS && michael@0: action.mType != Action.TYPE_SET_HANDLER) { michael@0: action.mShouldUpdate = mUpdateGecko; michael@0: } michael@0: if (mActions.isEmpty()) { michael@0: mActionsActive.acquireUninterruptibly(); michael@0: mActions.offer(action); michael@0: } else synchronized(this) { michael@0: // tryAcquire here in case Gecko thread has just released it michael@0: mActionsActive.tryAcquire(); michael@0: mActions.offer(action); michael@0: } michael@0: switch (action.mType) { michael@0: case Action.TYPE_EVENT: michael@0: case Action.TYPE_SET_SELECTION: michael@0: case Action.TYPE_SET_SPAN: michael@0: case Action.TYPE_REMOVE_SPAN: michael@0: case Action.TYPE_SET_HANDLER: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( michael@0: GeckoEvent.ImeAction.IME_SYNCHRONIZE)); michael@0: break; michael@0: case Action.TYPE_REPLACE_TEXT: michael@0: // try key events first michael@0: sendCharKeyEvents(action); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEReplaceEvent( michael@0: action.mStart, action.mEnd, action.mSequence.toString())); michael@0: break; michael@0: case Action.TYPE_ACKNOWLEDGE_FOCUS: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( michael@0: GeckoEvent.ImeAction.IME_ACKNOWLEDGE_FOCUS)); michael@0: break; michael@0: } michael@0: ++mIcUpdateSeqno; michael@0: } michael@0: michael@0: private KeyEvent [] synthesizeKeyEvents(CharSequence cs) { michael@0: try { michael@0: if (mKeyMap == null) { michael@0: mKeyMap = KeyCharacterMap.load( michael@0: Build.VERSION.SDK_INT < 11 ? KeyCharacterMap.ALPHA : michael@0: KeyCharacterMap.VIRTUAL_KEYBOARD); michael@0: } michael@0: } catch (Exception e) { michael@0: // KeyCharacterMap.UnavailableExcepton is not found on Gingerbread; michael@0: // besides, it seems like HC and ICS will throw something other than michael@0: // KeyCharacterMap.UnavailableExcepton; so use a generic Exception here michael@0: return null; michael@0: } michael@0: KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray()); michael@0: if (keyEvents == null || keyEvents.length == 0) { michael@0: return null; michael@0: } michael@0: return keyEvents; michael@0: } michael@0: michael@0: private void sendCharKeyEvents(Action action) { michael@0: if (action.mSequence.length() == 0 || michael@0: (action.mSequence instanceof Spannable && michael@0: ((Spannable)action.mSequence).nextSpanTransition( michael@0: -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) { michael@0: // Spans are not preserved when we use key events, michael@0: // so we need the sequence to not have any spans michael@0: return; michael@0: } michael@0: KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence); michael@0: if (keyEvents == null) { michael@0: return; michael@0: } michael@0: for (KeyEvent event : keyEvents) { michael@0: if (KeyEvent.isModifierKey(event.getKeyCode())) { michael@0: continue; michael@0: } michael@0: if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) { michael@0: continue; michael@0: } michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "sending: " + event); michael@0: } michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEKeyEvent(event)); michael@0: } michael@0: } michael@0: michael@0: void poll() { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnGeckoThread(); michael@0: } michael@0: if (mActions.isEmpty()) { michael@0: throw new IllegalStateException("empty actions queue"); michael@0: } michael@0: mActions.poll(); michael@0: // Don't bother locking if queue is not empty yet michael@0: if (mActions.isEmpty()) { michael@0: synchronized(this) { michael@0: if (mActions.isEmpty()) { michael@0: mActionsActive.release(); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: Action peek() { michael@0: if (DEBUG) { michael@0: ThreadUtils.assertOnGeckoThread(); michael@0: } michael@0: if (mActions.isEmpty()) { michael@0: throw new IllegalStateException("empty actions queue"); michael@0: } michael@0: return mActions.peek(); michael@0: } michael@0: michael@0: void syncWithGecko() { michael@0: if (DEBUG) { michael@0: assertOnIcThread(); michael@0: } michael@0: if (mFocused && !mActions.isEmpty()) { michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "syncWithGecko blocking on thread " + michael@0: Thread.currentThread().getName()); michael@0: } michael@0: mActionsActive.acquireUninterruptibly(); michael@0: mActionsActive.release(); michael@0: } else if (DEBUG && !mFocused) { michael@0: Log.d(LOGTAG, "skipped syncWithGecko (no focus)"); michael@0: } michael@0: } michael@0: michael@0: boolean isEmpty() { michael@0: return mActions.isEmpty(); michael@0: } michael@0: } michael@0: michael@0: GeckoEditable() { michael@0: mActionQueue = new ActionQueue(); michael@0: mSavedSelectionStart = -1; michael@0: mUpdateGecko = true; michael@0: michael@0: mText = new SpannableStringBuilder(); michael@0: mChangedText = new SpannableStringBuilder(); michael@0: michael@0: final Class[] PROXY_INTERFACES = { Editable.class }; michael@0: mProxy = (Editable)Proxy.newProxyInstance( michael@0: Editable.class.getClassLoader(), michael@0: PROXY_INTERFACES, this); michael@0: michael@0: LayerView v = GeckoAppShell.getLayerView(); michael@0: mListener = GeckoInputConnection.create(v, this); michael@0: michael@0: mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler(); michael@0: } michael@0: michael@0: private boolean onIcThread() { michael@0: return mIcRunHandler.getLooper() == Looper.myLooper(); michael@0: } michael@0: michael@0: private void assertOnIcThread() { michael@0: ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW); michael@0: } michael@0: michael@0: private void geckoPostToIc(Runnable runnable) { michael@0: mIcPostHandler.post(runnable); michael@0: } michael@0: michael@0: private void geckoUpdateGecko(final boolean force) { michael@0: /* We do not increment the seqno here, but only check it, because geckoUpdateGecko is a michael@0: request for update. If we incremented the seqno here, geckoUpdateGecko would have michael@0: prevented other updates from occurring */ michael@0: final int seqnoWhenPosted = mGeckoUpdateSeqno; michael@0: michael@0: geckoPostToIc(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mActionQueue.syncWithGecko(); michael@0: if (seqnoWhenPosted == mGeckoUpdateSeqno) { michael@0: icUpdateGecko(force); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private Object getField(Object obj, String field, Object def) { michael@0: try { michael@0: return obj.getClass().getField(field).get(obj); michael@0: } catch (Exception e) { michael@0: return def; michael@0: } michael@0: } michael@0: michael@0: private void icUpdateGecko(boolean force) { michael@0: michael@0: // Skip if receiving a repeated request, or michael@0: // if suppressing compositions during text selection. michael@0: if ((!force && mIcUpdateSeqno == mLastIcUpdateSeqno) || michael@0: mSuppressCompositions) { michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "icUpdateGecko() skipped"); michael@0: } michael@0: return; michael@0: } michael@0: mLastIcUpdateSeqno = mIcUpdateSeqno; michael@0: mActionQueue.syncWithGecko(); michael@0: michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "icUpdateGecko()"); michael@0: } michael@0: michael@0: final int selStart = mText.getSpanStart(Selection.SELECTION_START); michael@0: final int selEnd = mText.getSpanEnd(Selection.SELECTION_END); michael@0: int composingStart = mText.length(); michael@0: int composingEnd = 0; michael@0: Object[] spans = mText.getSpans(0, composingStart, Object.class); michael@0: michael@0: for (Object span : spans) { michael@0: if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { michael@0: composingStart = Math.min(composingStart, mText.getSpanStart(span)); michael@0: composingEnd = Math.max(composingEnd, mText.getSpanEnd(span)); michael@0: } michael@0: } michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd); michael@0: Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd); michael@0: } michael@0: if (composingStart >= composingEnd) { michael@0: if (selStart >= 0 && selEnd >= 0) { michael@0: GeckoAppShell.sendEventToGecko( michael@0: GeckoEvent.createIMESelectEvent(selStart, selEnd)); michael@0: } else { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( michael@0: GeckoEvent.ImeAction.IME_REMOVE_COMPOSITION)); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: if (selEnd >= composingStart && selEnd <= composingEnd) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMERangeEvent( michael@0: selEnd - composingStart, selEnd - composingStart, michael@0: GeckoEvent.IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0)); michael@0: } michael@0: int rangeStart = composingStart; michael@0: TextPaint tp = new TextPaint(); michael@0: TextPaint emptyTp = new TextPaint(); michael@0: // set initial foreground color to 0, because we check for tp.getColor() == 0 michael@0: // below to decide whether to pass a foreground color to Gecko michael@0: emptyTp.setColor(0); michael@0: do { michael@0: int rangeType, rangeStyles = 0, rangeLineStyle = GeckoEvent.IME_RANGE_LINE_NONE; michael@0: boolean rangeBoldLine = false; michael@0: int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0; michael@0: int rangeEnd = mText.nextSpanTransition(rangeStart, composingEnd, Object.class); michael@0: michael@0: if (selStart > rangeStart && selStart < rangeEnd) { michael@0: rangeEnd = selStart; michael@0: } else if (selEnd > rangeStart && selEnd < rangeEnd) { michael@0: rangeEnd = selEnd; michael@0: } michael@0: CharacterStyle[] styleSpans = michael@0: mText.getSpans(rangeStart, rangeEnd, CharacterStyle.class); michael@0: michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " + michael@0: rangeStart + "-" + rangeEnd); michael@0: } michael@0: michael@0: if (styleSpans.length == 0) { michael@0: rangeType = (selStart == rangeStart && selEnd == rangeEnd) michael@0: ? GeckoEvent.IME_RANGE_SELECTEDRAWTEXT michael@0: : GeckoEvent.IME_RANGE_RAWINPUT; michael@0: } else { michael@0: rangeType = (selStart == rangeStart && selEnd == rangeEnd) michael@0: ? GeckoEvent.IME_RANGE_SELECTEDCONVERTEDTEXT michael@0: : GeckoEvent.IME_RANGE_CONVERTEDTEXT; michael@0: tp.set(emptyTp); michael@0: for (CharacterStyle span : styleSpans) { michael@0: span.updateDrawState(tp); michael@0: } michael@0: int tpUnderlineColor = 0; michael@0: float tpUnderlineThickness = 0.0f; michael@0: // These TextPaint fields only exist on Android ICS+ and are not in the SDK michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0); michael@0: tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f); michael@0: } michael@0: if (tpUnderlineColor != 0) { michael@0: rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE | GeckoEvent.IME_RANGE_LINECOLOR; michael@0: rangeLineColor = tpUnderlineColor; michael@0: // Approximately translate underline thickness to what Gecko understands michael@0: if (tpUnderlineThickness <= 0.5f) { michael@0: rangeLineStyle = GeckoEvent.IME_RANGE_LINE_DOTTED; michael@0: } else { michael@0: rangeLineStyle = GeckoEvent.IME_RANGE_LINE_SOLID; michael@0: if (tpUnderlineThickness >= 2.0f) { michael@0: rangeBoldLine = true; michael@0: } michael@0: } michael@0: } else if (tp.isUnderlineText()) { michael@0: rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE; michael@0: rangeLineStyle = GeckoEvent.IME_RANGE_LINE_SOLID; michael@0: } michael@0: if (tp.getColor() != 0) { michael@0: rangeStyles |= GeckoEvent.IME_RANGE_FORECOLOR; michael@0: rangeForeColor = tp.getColor(); michael@0: } michael@0: if (tp.bgColor != 0) { michael@0: rangeStyles |= GeckoEvent.IME_RANGE_BACKCOLOR; michael@0: rangeBackColor = tp.bgColor; michael@0: } michael@0: } michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMERangeEvent( michael@0: rangeStart - composingStart, rangeEnd - composingStart, michael@0: rangeType, rangeStyles, rangeLineStyle, rangeBoldLine, michael@0: rangeForeColor, rangeBackColor, rangeLineColor)); michael@0: rangeStart = rangeEnd; michael@0: michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, " added " + rangeType + michael@0: " : " + Integer.toHexString(rangeStyles) + michael@0: " : " + Integer.toHexString(rangeForeColor) + michael@0: " : " + Integer.toHexString(rangeBackColor)); michael@0: } michael@0: } while (rangeStart < composingEnd); michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createIMECompositionEvent( michael@0: composingStart, composingEnd)); michael@0: } michael@0: michael@0: // GeckoEditableClient interface michael@0: michael@0: @Override michael@0: public void sendEvent(final GeckoEvent event) { michael@0: if (DEBUG) { michael@0: assertOnIcThread(); michael@0: Log.d(LOGTAG, "sendEvent(" + event + ")"); michael@0: } michael@0: /* michael@0: We are actually sending two events to Gecko here, michael@0: 1. Event from the event parameter (key event, etc.) michael@0: 2. Sync event from the mActionQueue.offer call michael@0: The first event is a normal GeckoEvent that does not reply back to us, michael@0: the second sync event will have a reply, during which we see that there is a pending michael@0: event-type action, and update the selection/composition/etc. accordingly. michael@0: */ michael@0: GeckoAppShell.sendEventToGecko(event); michael@0: mActionQueue.offer(new Action(Action.TYPE_EVENT)); michael@0: } michael@0: michael@0: @Override michael@0: public Editable getEditable() { michael@0: if (!onIcThread()) { michael@0: // Android may be holding an old InputConnection; ignore michael@0: if (DEBUG) { michael@0: Log.i(LOGTAG, "getEditable() called on non-IC thread"); michael@0: } michael@0: return null; michael@0: } michael@0: return mProxy; michael@0: } michael@0: michael@0: @Override michael@0: public void setUpdateGecko(boolean update) { michael@0: if (!onIcThread()) { michael@0: // Android may be holding an old InputConnection; ignore michael@0: if (DEBUG) { michael@0: Log.i(LOGTAG, "setUpdateGecko() called on non-IC thread"); michael@0: } michael@0: return; michael@0: } michael@0: if (update) { michael@0: icUpdateGecko(false); michael@0: } michael@0: mUpdateGecko = update; michael@0: } michael@0: michael@0: @Override michael@0: public void setSuppressKeyUp(boolean suppress) { michael@0: if (DEBUG) { michael@0: // only used by key event handler michael@0: ThreadUtils.assertOnUiThread(); michael@0: } michael@0: // Suppress key up event generated as a result of michael@0: // translating characters to key events michael@0: mSuppressKeyUp = suppress; michael@0: } michael@0: michael@0: @Override michael@0: public Handler getInputConnectionHandler() { michael@0: // Can be called from either UI thread or IC thread; michael@0: // care must be taken to avoid race conditions michael@0: return mIcRunHandler; michael@0: } michael@0: michael@0: @Override michael@0: public boolean setInputConnectionHandler(Handler handler) { michael@0: if (handler == mIcPostHandler) { michael@0: return true; michael@0: } michael@0: if (!mFocused) { michael@0: return false; michael@0: } michael@0: if (DEBUG) { michael@0: assertOnIcThread(); michael@0: } michael@0: // There are three threads at this point: Gecko thread, old IC thread, and new IC michael@0: // thread, and we want to safely switch from old IC thread to new IC thread. michael@0: // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that michael@0: // the Gecko thread is stopped at a known point. At the same time, the old IC michael@0: // thread blocks on the action; this ensures that the old IC thread is stopped at michael@0: // a known point. Finally, inside the Gecko thread, we post a Runnable to the old michael@0: // IC thread; this Runnable switches from old IC thread to new IC thread. We michael@0: // switch IC thread on the old IC thread to ensure any pending Runnables on the michael@0: // old IC thread are processed before we switch over. Inside the Gecko thread, we michael@0: // also post a Runnable to the new IC thread; this Runnable blocks until the michael@0: // switch is complete; this ensures that the new IC thread won't accept michael@0: // InputConnection calls until after the switch. michael@0: mActionQueue.offer(Action.newSetHandler(handler)); michael@0: mActionQueue.syncWithGecko(); michael@0: return true; michael@0: } michael@0: michael@0: private void geckoSetIcHandler(final Handler newHandler) { michael@0: geckoPostToIc(new Runnable() { // posting to old IC thread michael@0: @Override michael@0: public void run() { michael@0: synchronized (newHandler) { michael@0: mIcRunHandler = newHandler; michael@0: newHandler.notify(); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // At this point, all future Runnables should be posted to the new IC thread, but michael@0: // we don't switch mIcRunHandler yet because there may be pending Runnables on the michael@0: // old IC thread still waiting to run. michael@0: mIcPostHandler = newHandler; michael@0: michael@0: geckoPostToIc(new Runnable() { // posting to new IC thread michael@0: @Override michael@0: public void run() { michael@0: synchronized (newHandler) { michael@0: while (mIcRunHandler != newHandler) { michael@0: try { michael@0: newHandler.wait(); michael@0: } catch (InterruptedException e) { michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // GeckoEditableListener interface michael@0: michael@0: private void geckoActionReply() { michael@0: if (DEBUG) { michael@0: // GeckoEditableListener methods should all be called from the Gecko thread michael@0: ThreadUtils.assertOnGeckoThread(); michael@0: } michael@0: final Action action = mActionQueue.peek(); michael@0: michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "reply: Action(" + michael@0: getConstantName(Action.class, "TYPE_", action.mType) + ")"); michael@0: } michael@0: switch (action.mType) { michael@0: case Action.TYPE_SET_SELECTION: michael@0: final int len = mText.length(); michael@0: final int curStart = Selection.getSelectionStart(mText); michael@0: final int curEnd = Selection.getSelectionEnd(mText); michael@0: // start == -1 when the start offset should remain the same michael@0: // end == -1 when the end offset should remain the same michael@0: final int selStart = Math.min(action.mStart < 0 ? curStart : action.mStart, len); michael@0: final int selEnd = Math.min(action.mEnd < 0 ? curEnd : action.mEnd, len); michael@0: michael@0: if (selStart < action.mStart || selEnd < action.mEnd) { michael@0: Log.w(LOGTAG, "IME sync error: selection out of bounds"); michael@0: } michael@0: Selection.setSelection(mText, selStart, selEnd); michael@0: geckoPostToIc(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mActionQueue.syncWithGecko(); michael@0: final int start = Selection.getSelectionStart(mText); michael@0: final int end = Selection.getSelectionEnd(mText); michael@0: if (selStart == start && selEnd == end) { michael@0: // There has not been another new selection in the mean time that michael@0: // made this notification out-of-date michael@0: mListener.onSelectionChange(start, end); michael@0: } michael@0: } michael@0: }); michael@0: break; michael@0: case Action.TYPE_SET_SPAN: michael@0: mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags); michael@0: break; michael@0: case Action.TYPE_SET_HANDLER: michael@0: geckoSetIcHandler(action.mHandler); michael@0: break; michael@0: } michael@0: if (action.mShouldUpdate) { michael@0: geckoUpdateGecko(false); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void notifyIME(final int type) { michael@0: if (DEBUG) { michael@0: // GeckoEditableListener methods should all be called from the Gecko thread michael@0: ThreadUtils.assertOnGeckoThread(); michael@0: // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply() michael@0: if (type != NOTIFY_IME_REPLY_EVENT) { michael@0: Log.d(LOGTAG, "notifyIME(" + michael@0: getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) + michael@0: ")"); michael@0: } michael@0: } michael@0: if (type == NOTIFY_IME_REPLY_EVENT) { michael@0: try { michael@0: if (mFocused) { michael@0: // When mFocused is false, the reply is for a stale action, michael@0: // and we should not do anything michael@0: geckoActionReply(); michael@0: } else if (DEBUG) { michael@0: Log.d(LOGTAG, "discarding stale reply"); michael@0: } michael@0: } finally { michael@0: // Ensure action is always removed from queue michael@0: // even if stale action results in exception in geckoActionReply michael@0: mActionQueue.poll(); michael@0: } michael@0: return; michael@0: } michael@0: geckoPostToIc(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: if (type == NOTIFY_IME_OF_BLUR) { michael@0: mFocused = false; michael@0: } else if (type == NOTIFY_IME_OF_FOCUS) { michael@0: mFocused = true; michael@0: // Unmask events on the Gecko side michael@0: mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS)); michael@0: } michael@0: // Make sure there are no other things going on. If we sent michael@0: // GeckoEvent.IME_ACKNOWLEDGE_FOCUS, this line also makes us michael@0: // wait for Gecko to update us on the newly focused content michael@0: mActionQueue.syncWithGecko(); michael@0: mListener.notifyIME(type); michael@0: } michael@0: }); michael@0: michael@0: // Register/unregister Gecko-side text selection listeners michael@0: // and update the mGeckoFocused flag. michael@0: if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) { michael@0: // Check for focus here because Gecko may send us a blur before a focus in some michael@0: // cases, and we don't want to unregister an event that was not registered. michael@0: mGeckoFocused = false; michael@0: mSuppressCompositions = false; michael@0: GeckoAppShell.getEventDispatcher(). michael@0: unregisterEventListener("TextSelection:DraggingHandle", this); michael@0: } else if (type == NOTIFY_IME_OF_FOCUS) { michael@0: mGeckoFocused = true; michael@0: mSuppressCompositions = false; michael@0: GeckoAppShell.getEventDispatcher(). michael@0: registerEventListener("TextSelection:DraggingHandle", this); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void notifyIMEContext(final int state, final String typeHint, michael@0: final String modeHint, final String actionHint) { michael@0: // Because we want to be able to bind GeckoEditable to the newest LayerView instance, michael@0: // this can be called from the Java IC thread in addition to the Gecko thread. michael@0: if (DEBUG) { michael@0: Log.d(LOGTAG, "notifyIMEContext(" + michael@0: getConstantName(GeckoEditableListener.class, "IME_STATE_", state) + michael@0: ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")"); michael@0: } michael@0: geckoPostToIc(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: // Make sure there are no other things going on michael@0: mActionQueue.syncWithGecko(); michael@0: // Set InputConnectionHandler in notifyIMEContext because michael@0: // GeckoInputConnection.notifyIMEContext calls restartInput() which will invoke michael@0: // InputConnectionHandler.onCreateInputConnection michael@0: LayerView v = GeckoAppShell.getLayerView(); michael@0: if (v != null) { michael@0: mListener = GeckoInputConnection.create(v, GeckoEditable.this); michael@0: v.setInputConnectionHandler((InputConnectionHandler)mListener); michael@0: mListener.notifyIMEContext(state, typeHint, modeHint, actionHint); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void onSelectionChange(final int start, final int end) { michael@0: if (DEBUG) { michael@0: // GeckoEditableListener methods should all be called from the Gecko thread michael@0: ThreadUtils.assertOnGeckoThread(); michael@0: Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")"); michael@0: } michael@0: if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) { michael@0: throw new IllegalArgumentException("invalid selection notification range: " + michael@0: start + " to " + end + ", length: " + mText.length()); michael@0: } michael@0: final int seqnoWhenPosted = ++mGeckoUpdateSeqno; michael@0: michael@0: /* An event (keypress, etc.) has potentially changed the selection, michael@0: synchronize the selection here. There is not a race with the IC thread michael@0: because the IC thread should be blocked on the event action */ michael@0: if (!mActionQueue.isEmpty() && michael@0: mActionQueue.peek().mType == Action.TYPE_EVENT) { michael@0: Selection.setSelection(mText, start, end); michael@0: return; michael@0: } michael@0: michael@0: geckoPostToIc(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mActionQueue.syncWithGecko(); michael@0: /* check to see there has not been another action that potentially changed the michael@0: selection. If so, we can skip this update because we know there is another michael@0: update right after this one that will replace the effect of this update */ michael@0: if (mGeckoUpdateSeqno == seqnoWhenPosted) { michael@0: /* In this case, Gecko's selection has changed and it's notifying us to change michael@0: Java's selection. In the normal case, whenever Java's selection changes, michael@0: we go back and set Gecko's selection as well. However, in this case, michael@0: since Gecko's selection is already up-to-date, we skip this step. */ michael@0: boolean oldUpdateGecko = mUpdateGecko; michael@0: mUpdateGecko = false; michael@0: Selection.setSelection(mProxy, start, end); michael@0: mUpdateGecko = oldUpdateGecko; michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void geckoReplaceText(int start, int oldEnd, CharSequence newText) { michael@0: // Don't use replace() because Gingerbread has a bug where if the replaced text michael@0: // has the same spans as the original text, the spans will end up being deleted michael@0: mText.delete(start, oldEnd); michael@0: mText.insert(start, newText); michael@0: } michael@0: michael@0: @Override michael@0: public void onTextChange(final String text, final int start, michael@0: final int unboundedOldEnd, final int unboundedNewEnd) { michael@0: if (DEBUG) { michael@0: // GeckoEditableListener methods should all be called from the Gecko thread michael@0: ThreadUtils.assertOnGeckoThread(); michael@0: StringBuilder sb = new StringBuilder("onTextChange("); michael@0: debugAppend(sb, text); michael@0: sb.append(", ").append(start).append(", ") michael@0: .append(unboundedOldEnd).append(", ") michael@0: .append(unboundedNewEnd).append(")"); michael@0: Log.d(LOGTAG, sb.toString()); michael@0: } michael@0: if (start < 0 || start > unboundedOldEnd) { michael@0: throw new IllegalArgumentException("invalid text notification range: " + michael@0: start + " to " + unboundedOldEnd); michael@0: } michael@0: /* For the "end" parameters, Gecko can pass in a large michael@0: number to denote "end of the text". Fix that here */ michael@0: final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd; michael@0: // new end should always match text michael@0: if (start != 0 && unboundedNewEnd != (start + text.length())) { michael@0: throw new IllegalArgumentException("newEnd does not match text: " + michael@0: unboundedNewEnd + " vs " + (start + text.length())); michael@0: } michael@0: final int newEnd = start + text.length(); michael@0: michael@0: /* Text changes affect the selection as well, and we may not receive another selection michael@0: update as a result of selection notification masking on the Gecko side; therefore, michael@0: in order to prevent previous stale selection notifications from occurring, we need michael@0: to increment the seqno here as well */ michael@0: ++mGeckoUpdateSeqno; michael@0: michael@0: mChangedText.clearSpans(); michael@0: mChangedText.replace(0, mChangedText.length(), text); michael@0: // Preserve as many spans as possible michael@0: TextUtils.copySpansFrom(mText, start, Math.min(oldEnd, newEnd), michael@0: Object.class, mChangedText, 0); michael@0: michael@0: if (!mActionQueue.isEmpty()) { michael@0: final Action action = mActionQueue.peek(); michael@0: if (action.mType == Action.TYPE_REPLACE_TEXT && michael@0: start <= action.mStart && michael@0: action.mStart + action.mSequence.length() <= newEnd) { michael@0: michael@0: // actionNewEnd is the new end of the original replacement action michael@0: final int actionNewEnd = action.mStart + action.mSequence.length(); michael@0: int selStart = Selection.getSelectionStart(mText); michael@0: int selEnd = Selection.getSelectionEnd(mText); michael@0: michael@0: // Replace old spans with new spans michael@0: mChangedText.replace(action.mStart - start, actionNewEnd - start, michael@0: action.mSequence); michael@0: geckoReplaceText(start, oldEnd, mChangedText); michael@0: michael@0: // delete/insert above might have moved our selection to somewhere else michael@0: // this happens when the Gecko text change covers a larger range than michael@0: // the original replacement action. Fix selection here michael@0: if (selStart >= start && selStart <= oldEnd) { michael@0: selStart = selStart < action.mStart ? selStart : michael@0: selStart < action.mEnd ? actionNewEnd : michael@0: selStart + actionNewEnd - action.mEnd; michael@0: mText.setSpan(Selection.SELECTION_START, selStart, selStart, michael@0: Spanned.SPAN_POINT_POINT); michael@0: } michael@0: if (selEnd >= start && selEnd <= oldEnd) { michael@0: selEnd = selEnd < action.mStart ? selEnd : michael@0: selEnd < action.mEnd ? actionNewEnd : michael@0: selEnd + actionNewEnd - action.mEnd; michael@0: mText.setSpan(Selection.SELECTION_END, selEnd, selEnd, michael@0: Spanned.SPAN_POINT_POINT); michael@0: } michael@0: } else { michael@0: geckoReplaceText(start, oldEnd, mChangedText); michael@0: } michael@0: } else { michael@0: geckoReplaceText(start, oldEnd, mChangedText); michael@0: } michael@0: geckoPostToIc(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mListener.onTextChange(text, start, oldEnd, newEnd); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // InvocationHandler interface michael@0: michael@0: static String getConstantName(Class cls, String prefix, Object value) { michael@0: for (Field fld : cls.getDeclaredFields()) { michael@0: try { michael@0: if (fld.getName().startsWith(prefix) && michael@0: fld.get(null).equals(value)) { michael@0: return fld.getName(); michael@0: } michael@0: } catch (IllegalAccessException e) { michael@0: } michael@0: } michael@0: return String.valueOf(value); michael@0: } michael@0: michael@0: static StringBuilder debugAppend(StringBuilder sb, Object obj) { michael@0: if (obj == null) { michael@0: sb.append("null"); michael@0: } else if (obj instanceof GeckoEditable) { michael@0: sb.append("GeckoEditable"); michael@0: } else if (Proxy.isProxyClass(obj.getClass())) { michael@0: debugAppend(sb, Proxy.getInvocationHandler(obj)); michael@0: } else if (obj instanceof CharSequence) { michael@0: sb.append("\"").append(obj.toString().replace('\n', '\u21b2')).append("\""); michael@0: } else if (obj.getClass().isArray()) { michael@0: sb.append(obj.getClass().getComponentType().getSimpleName()).append("[") michael@0: .append(java.lang.reflect.Array.getLength(obj)).append("]"); michael@0: } else { michael@0: sb.append(obj.toString()); michael@0: } michael@0: return sb; michael@0: } michael@0: michael@0: @Override michael@0: public Object invoke(Object proxy, Method method, Object[] args) michael@0: throws Throwable { michael@0: Object target; michael@0: final Class methodInterface = method.getDeclaringClass(); michael@0: if (DEBUG) { michael@0: // Editable methods should all be called from the IC thread michael@0: assertOnIcThread(); michael@0: } michael@0: if (methodInterface == Editable.class || michael@0: methodInterface == Appendable.class || michael@0: methodInterface == Spannable.class) { michael@0: // Method alters the Editable; route calls to our implementation michael@0: target = this; michael@0: } else { michael@0: // Method queries the Editable; must sync with Gecko first michael@0: // then call on the inner Editable itself michael@0: mActionQueue.syncWithGecko(); michael@0: target = mText; michael@0: } michael@0: Object ret; michael@0: try { michael@0: ret = method.invoke(target, args); michael@0: } catch (InvocationTargetException e) { michael@0: // Bug 817386 michael@0: // Most likely Gecko has changed the text while GeckoInputConnection is michael@0: // trying to access the text. If we pass through the exception here, Fennec michael@0: // will crash due to a lack of exception handler. Log the exception and michael@0: // return an empty value instead. michael@0: if (!(e.getCause() instanceof IndexOutOfBoundsException)) { michael@0: // Only handle IndexOutOfBoundsException for now, michael@0: // as other exceptions might signal other bugs michael@0: throw e; michael@0: } michael@0: Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause()); michael@0: Class retClass = method.getReturnType(); michael@0: if (retClass == Character.TYPE) { michael@0: ret = '\0'; michael@0: } else if (retClass == Integer.TYPE) { michael@0: ret = 0; michael@0: } else if (retClass == String.class) { michael@0: ret = ""; michael@0: } else { michael@0: ret = null; michael@0: } michael@0: } michael@0: if (DEBUG) { michael@0: StringBuilder log = new StringBuilder(method.getName()); michael@0: log.append("("); michael@0: for (Object arg : args) { michael@0: debugAppend(log, arg).append(", "); michael@0: } michael@0: if (args.length > 0) { michael@0: log.setLength(log.length() - 2); michael@0: } michael@0: if (method.getReturnType().equals(Void.TYPE)) { michael@0: log.append(")"); michael@0: } else { michael@0: debugAppend(log.append(") = "), ret); michael@0: } michael@0: Log.d(LOGTAG, log.toString()); michael@0: } michael@0: return ret; michael@0: } michael@0: michael@0: // Spannable interface michael@0: michael@0: @Override michael@0: public void removeSpan(Object what) { michael@0: if (what == Selection.SELECTION_START || michael@0: what == Selection.SELECTION_END) { michael@0: Log.w(LOGTAG, "selection removed with removeSpan()"); michael@0: } michael@0: if (mText.getSpanStart(what) >= 0) { // only remove if it's there michael@0: // Okay to remove immediately michael@0: mText.removeSpan(what); michael@0: mActionQueue.offer(new Action(Action.TYPE_REMOVE_SPAN)); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void setSpan(Object what, int start, int end, int flags) { michael@0: if (what == Selection.SELECTION_START) { michael@0: if ((flags & Spanned.SPAN_INTERMEDIATE) != 0) { michael@0: // We will get the end offset next, just save the start for now michael@0: mSavedSelectionStart = start; michael@0: } else { michael@0: mActionQueue.offer(Action.newSetSelection(start, -1)); michael@0: } michael@0: } else if (what == Selection.SELECTION_END) { michael@0: mActionQueue.offer(Action.newSetSelection(mSavedSelectionStart, end)); michael@0: mSavedSelectionStart = -1; michael@0: } else { michael@0: mActionQueue.offer(Action.newSetSpan(what, start, end, flags)); michael@0: } michael@0: } michael@0: michael@0: // Appendable interface michael@0: michael@0: @Override michael@0: public Editable append(CharSequence text) { michael@0: return replace(mProxy.length(), mProxy.length(), text, 0, text.length()); michael@0: } michael@0: michael@0: @Override michael@0: public Editable append(CharSequence text, int start, int end) { michael@0: return replace(mProxy.length(), mProxy.length(), text, start, end); michael@0: } michael@0: michael@0: @Override michael@0: public Editable append(char text) { michael@0: return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1); michael@0: } michael@0: michael@0: // Editable interface michael@0: michael@0: @Override michael@0: public InputFilter[] getFilters() { michael@0: return mFilters; michael@0: } michael@0: michael@0: @Override michael@0: public void setFilters(InputFilter[] filters) { michael@0: mFilters = filters; michael@0: } michael@0: michael@0: @Override michael@0: public void clearSpans() { michael@0: /* XXX this clears the selection spans too, michael@0: but there is no way to clear the corresponding selection in Gecko */ michael@0: Log.w(LOGTAG, "selection cleared with clearSpans()"); michael@0: mText.clearSpans(); michael@0: } michael@0: michael@0: @Override michael@0: public Editable replace(int st, int en, michael@0: CharSequence source, int start, int end) { michael@0: michael@0: CharSequence text = source; michael@0: if (start < 0 || start > end || end > text.length()) { michael@0: throw new IllegalArgumentException("invalid replace offsets: " + michael@0: start + " to " + end + ", length: " + text.length()); michael@0: } michael@0: if (start != 0 || end != text.length()) { michael@0: text = text.subSequence(start, end); michael@0: } michael@0: if (mFilters != null) { michael@0: // Filter text before sending the request to Gecko michael@0: for (int i = 0; i < mFilters.length; ++i) { michael@0: final CharSequence cs = mFilters[i].filter( michael@0: text, 0, text.length(), mProxy, st, en); michael@0: if (cs != null) { michael@0: text = cs; michael@0: } michael@0: } michael@0: } michael@0: if (text == source) { michael@0: // Always create a copy michael@0: text = new SpannableString(source); michael@0: } michael@0: mActionQueue.offer(Action.newReplaceText(text, michael@0: Math.min(st, en), Math.max(st, en))); michael@0: return mProxy; michael@0: } michael@0: michael@0: @Override michael@0: public void clear() { michael@0: replace(0, mProxy.length(), "", 0, 0); michael@0: } michael@0: michael@0: @Override michael@0: public Editable delete(int st, int en) { michael@0: return replace(st, en, "", 0, 0); michael@0: } michael@0: michael@0: @Override michael@0: public Editable insert(int where, CharSequence text, michael@0: int start, int end) { michael@0: return replace(where, where, text, start, end); michael@0: } michael@0: michael@0: @Override michael@0: public Editable insert(int where, CharSequence text) { michael@0: return replace(where, where, text, 0, text.length()); michael@0: } michael@0: michael@0: @Override michael@0: public Editable replace(int st, int en, CharSequence text) { michael@0: return replace(st, en, text, 0, text.length()); michael@0: } michael@0: michael@0: /* GetChars interface */ michael@0: michael@0: @Override michael@0: public void getChars(int start, int end, char[] dest, int destoff) { michael@0: /* overridden Editable interface methods in GeckoEditable must not be called directly michael@0: outside of GeckoEditable. Instead, the call must go through mProxy, which ensures michael@0: that Java is properly synchronized with Gecko */ michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: /* Spanned interface */ michael@0: michael@0: @Override michael@0: public int getSpanEnd(Object tag) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: public int getSpanFlags(Object tag) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: public int getSpanStart(Object tag) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: public T[] getSpans(int start, int end, Class type) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration michael@0: public int nextSpanTransition(int start, int limit, Class type) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: /* CharSequence interface */ michael@0: michael@0: @Override michael@0: public char charAt(int index) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: public int length() { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: public CharSequence subSequence(int start, int end) { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: throw new UnsupportedOperationException("method must be called through mProxy"); michael@0: } michael@0: michael@0: // GeckoEventListener implementation michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: if (!"TextSelection:DraggingHandle".equals(event)) { michael@0: return; michael@0: } michael@0: michael@0: mSuppressCompositions = message.optBoolean("dragging", false); michael@0: } michael@0: } michael@0: