1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/GeckoEditable.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1238 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.gfx.InputConnectionHandler; 1.12 +import org.mozilla.gecko.gfx.LayerView; 1.13 +import org.mozilla.gecko.util.GeckoEventListener; 1.14 +import org.mozilla.gecko.util.ThreadUtils; 1.15 +import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; 1.16 + 1.17 +import org.json.JSONObject; 1.18 + 1.19 +import android.os.Build; 1.20 +import android.os.Handler; 1.21 +import android.os.Looper; 1.22 +import android.text.Editable; 1.23 +import android.text.InputFilter; 1.24 +import android.text.Selection; 1.25 +import android.text.Spannable; 1.26 +import android.text.SpannableString; 1.27 +import android.text.SpannableStringBuilder; 1.28 +import android.text.Spanned; 1.29 +import android.text.TextPaint; 1.30 +import android.text.TextUtils; 1.31 +import android.text.style.CharacterStyle; 1.32 +import android.util.Log; 1.33 +import android.view.KeyCharacterMap; 1.34 +import android.view.KeyEvent; 1.35 + 1.36 +import java.lang.reflect.Field; 1.37 +import java.lang.reflect.InvocationHandler; 1.38 +import java.lang.reflect.InvocationTargetException; 1.39 +import java.lang.reflect.Method; 1.40 +import java.lang.reflect.Proxy; 1.41 +import java.util.concurrent.ConcurrentLinkedQueue; 1.42 +import java.util.concurrent.Semaphore; 1.43 + 1.44 +// interface for the IC thread 1.45 +interface GeckoEditableClient { 1.46 + void sendEvent(GeckoEvent event); 1.47 + Editable getEditable(); 1.48 + void setUpdateGecko(boolean update); 1.49 + void setSuppressKeyUp(boolean suppress); 1.50 + Handler getInputConnectionHandler(); 1.51 + boolean setInputConnectionHandler(Handler handler); 1.52 +} 1.53 + 1.54 +/* interface for the Editable to listen to the Gecko thread 1.55 + and also for the IC thread to listen to the Editable */ 1.56 +interface GeckoEditableListener { 1.57 + // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko 1.58 + final int NOTIFY_IME_OPEN_VKB = -2; 1.59 + final int NOTIFY_IME_REPLY_EVENT = -1; 1.60 + final int NOTIFY_IME_OF_FOCUS = 1; 1.61 + final int NOTIFY_IME_OF_BLUR = 2; 1.62 + final int NOTIFY_IME_TO_COMMIT_COMPOSITION = 7; 1.63 + final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 8; 1.64 + // IME enabled state for notifyIMEContext() 1.65 + final int IME_STATE_DISABLED = 0; 1.66 + final int IME_STATE_ENABLED = 1; 1.67 + final int IME_STATE_PASSWORD = 2; 1.68 + final int IME_STATE_PLUGIN = 3; 1.69 + 1.70 + void notifyIME(int type); 1.71 + void notifyIMEContext(int state, String typeHint, 1.72 + String modeHint, String actionHint); 1.73 + void onSelectionChange(int start, int end); 1.74 + void onTextChange(String text, int start, int oldEnd, int newEnd); 1.75 +} 1.76 + 1.77 +/* 1.78 + GeckoEditable implements only some functions of Editable 1.79 + The field mText contains the actual underlying 1.80 + SpannableStringBuilder/Editable that contains our text. 1.81 +*/ 1.82 +final class GeckoEditable 1.83 + implements InvocationHandler, Editable, 1.84 + GeckoEditableClient, GeckoEditableListener, GeckoEventListener { 1.85 + 1.86 + private static final boolean DEBUG = false; 1.87 + private static final String LOGTAG = "GeckoEditable"; 1.88 + 1.89 + // Filters to implement Editable's filtering functionality 1.90 + private InputFilter[] mFilters; 1.91 + 1.92 + private final SpannableStringBuilder mText; 1.93 + private final SpannableStringBuilder mChangedText; 1.94 + private final Editable mProxy; 1.95 + private final ActionQueue mActionQueue; 1.96 + 1.97 + // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables 1.98 + // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to 1.99 + // The two can be different when switching from one handler to another 1.100 + private Handler mIcRunHandler; 1.101 + private Handler mIcPostHandler; 1.102 + 1.103 + private GeckoEditableListener mListener; 1.104 + private int mSavedSelectionStart; 1.105 + private volatile int mGeckoUpdateSeqno; 1.106 + private int mIcUpdateSeqno; 1.107 + private int mLastIcUpdateSeqno; 1.108 + private boolean mUpdateGecko; 1.109 + private boolean mFocused; // Used by IC thread 1.110 + private boolean mGeckoFocused; // Used by Gecko thread 1.111 + private volatile boolean mSuppressCompositions; 1.112 + private volatile boolean mSuppressKeyUp; 1.113 + 1.114 + /* An action that alters the Editable 1.115 + 1.116 + Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko 1.117 + thread, the action stays on top of mActions queue. After the Gecko event is processed and 1.118 + replied, the action is removed from the queue 1.119 + */ 1.120 + private static final class Action { 1.121 + // For input events (keypress, etc.); use with IME_SYNCHRONIZE 1.122 + static final int TYPE_EVENT = 0; 1.123 + // For Editable.replace() call; use with IME_REPLACE_TEXT 1.124 + static final int TYPE_REPLACE_TEXT = 1; 1.125 + /* For Editable.setSpan(Selection...) call; use with IME_SYNCHRONIZE 1.126 + Note that we don't use this with IME_SET_SELECTION because we don't want to update the 1.127 + Gecko selection at the point of this action. The Gecko selection is updated only after 1.128 + IC has updated its selection (during IME_SYNCHRONIZE reply) */ 1.129 + static final int TYPE_SET_SELECTION = 2; 1.130 + // For Editable.setSpan() call; use with IME_SYNCHRONIZE 1.131 + static final int TYPE_SET_SPAN = 3; 1.132 + // For Editable.removeSpan() call; use with IME_SYNCHRONIZE 1.133 + static final int TYPE_REMOVE_SPAN = 4; 1.134 + // For focus events (in notifyIME); use with IME_ACKNOWLEDGE_FOCUS 1.135 + static final int TYPE_ACKNOWLEDGE_FOCUS = 5; 1.136 + // For switching handler; use with IME_SYNCHRONIZE 1.137 + static final int TYPE_SET_HANDLER = 6; 1.138 + 1.139 + final int mType; 1.140 + int mStart; 1.141 + int mEnd; 1.142 + CharSequence mSequence; 1.143 + Object mSpanObject; 1.144 + int mSpanFlags; 1.145 + boolean mShouldUpdate; 1.146 + Handler mHandler; 1.147 + 1.148 + Action(int type) { 1.149 + mType = type; 1.150 + } 1.151 + 1.152 + static Action newReplaceText(CharSequence text, int start, int end) { 1.153 + if (start < 0 || start > end) { 1.154 + throw new IllegalArgumentException( 1.155 + "invalid replace text offsets: " + start + " to " + end); 1.156 + } 1.157 + final Action action = new Action(TYPE_REPLACE_TEXT); 1.158 + action.mSequence = text; 1.159 + action.mStart = start; 1.160 + action.mEnd = end; 1.161 + return action; 1.162 + } 1.163 + 1.164 + static Action newSetSelection(int start, int end) { 1.165 + // start == -1 when the start offset should remain the same 1.166 + // end == -1 when the end offset should remain the same 1.167 + if (start < -1 || end < -1) { 1.168 + throw new IllegalArgumentException( 1.169 + "invalid selection offsets: " + start + " to " + end); 1.170 + } 1.171 + final Action action = new Action(TYPE_SET_SELECTION); 1.172 + action.mStart = start; 1.173 + action.mEnd = end; 1.174 + return action; 1.175 + } 1.176 + 1.177 + static Action newSetSpan(Object object, int start, int end, int flags) { 1.178 + if (start < 0 || start > end) { 1.179 + throw new IllegalArgumentException( 1.180 + "invalid span offsets: " + start + " to " + end); 1.181 + } 1.182 + final Action action = new Action(TYPE_SET_SPAN); 1.183 + action.mSpanObject = object; 1.184 + action.mStart = start; 1.185 + action.mEnd = end; 1.186 + action.mSpanFlags = flags; 1.187 + return action; 1.188 + } 1.189 + 1.190 + static Action newSetHandler(Handler handler) { 1.191 + final Action action = new Action(TYPE_SET_HANDLER); 1.192 + action.mHandler = handler; 1.193 + return action; 1.194 + } 1.195 + } 1.196 + 1.197 + /* Queue of editing actions sent to Gecko thread that 1.198 + the Gecko thread has not responded to yet */ 1.199 + private final class ActionQueue { 1.200 + private final ConcurrentLinkedQueue<Action> mActions; 1.201 + private final Semaphore mActionsActive; 1.202 + private KeyCharacterMap mKeyMap; 1.203 + 1.204 + ActionQueue() { 1.205 + mActions = new ConcurrentLinkedQueue<Action>(); 1.206 + mActionsActive = new Semaphore(1); 1.207 + } 1.208 + 1.209 + void offer(Action action) { 1.210 + if (DEBUG) { 1.211 + assertOnIcThread(); 1.212 + Log.d(LOGTAG, "offer: Action(" + 1.213 + getConstantName(Action.class, "TYPE_", action.mType) + ")"); 1.214 + } 1.215 + /* Events don't need update because they generate text/selection 1.216 + notifications which will do the updating for us */ 1.217 + if (action.mType != Action.TYPE_EVENT && 1.218 + action.mType != Action.TYPE_ACKNOWLEDGE_FOCUS && 1.219 + action.mType != Action.TYPE_SET_HANDLER) { 1.220 + action.mShouldUpdate = mUpdateGecko; 1.221 + } 1.222 + if (mActions.isEmpty()) { 1.223 + mActionsActive.acquireUninterruptibly(); 1.224 + mActions.offer(action); 1.225 + } else synchronized(this) { 1.226 + // tryAcquire here in case Gecko thread has just released it 1.227 + mActionsActive.tryAcquire(); 1.228 + mActions.offer(action); 1.229 + } 1.230 + switch (action.mType) { 1.231 + case Action.TYPE_EVENT: 1.232 + case Action.TYPE_SET_SELECTION: 1.233 + case Action.TYPE_SET_SPAN: 1.234 + case Action.TYPE_REMOVE_SPAN: 1.235 + case Action.TYPE_SET_HANDLER: 1.236 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( 1.237 + GeckoEvent.ImeAction.IME_SYNCHRONIZE)); 1.238 + break; 1.239 + case Action.TYPE_REPLACE_TEXT: 1.240 + // try key events first 1.241 + sendCharKeyEvents(action); 1.242 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEReplaceEvent( 1.243 + action.mStart, action.mEnd, action.mSequence.toString())); 1.244 + break; 1.245 + case Action.TYPE_ACKNOWLEDGE_FOCUS: 1.246 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( 1.247 + GeckoEvent.ImeAction.IME_ACKNOWLEDGE_FOCUS)); 1.248 + break; 1.249 + } 1.250 + ++mIcUpdateSeqno; 1.251 + } 1.252 + 1.253 + private KeyEvent [] synthesizeKeyEvents(CharSequence cs) { 1.254 + try { 1.255 + if (mKeyMap == null) { 1.256 + mKeyMap = KeyCharacterMap.load( 1.257 + Build.VERSION.SDK_INT < 11 ? KeyCharacterMap.ALPHA : 1.258 + KeyCharacterMap.VIRTUAL_KEYBOARD); 1.259 + } 1.260 + } catch (Exception e) { 1.261 + // KeyCharacterMap.UnavailableExcepton is not found on Gingerbread; 1.262 + // besides, it seems like HC and ICS will throw something other than 1.263 + // KeyCharacterMap.UnavailableExcepton; so use a generic Exception here 1.264 + return null; 1.265 + } 1.266 + KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray()); 1.267 + if (keyEvents == null || keyEvents.length == 0) { 1.268 + return null; 1.269 + } 1.270 + return keyEvents; 1.271 + } 1.272 + 1.273 + private void sendCharKeyEvents(Action action) { 1.274 + if (action.mSequence.length() == 0 || 1.275 + (action.mSequence instanceof Spannable && 1.276 + ((Spannable)action.mSequence).nextSpanTransition( 1.277 + -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) { 1.278 + // Spans are not preserved when we use key events, 1.279 + // so we need the sequence to not have any spans 1.280 + return; 1.281 + } 1.282 + KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence); 1.283 + if (keyEvents == null) { 1.284 + return; 1.285 + } 1.286 + for (KeyEvent event : keyEvents) { 1.287 + if (KeyEvent.isModifierKey(event.getKeyCode())) { 1.288 + continue; 1.289 + } 1.290 + if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) { 1.291 + continue; 1.292 + } 1.293 + if (DEBUG) { 1.294 + Log.d(LOGTAG, "sending: " + event); 1.295 + } 1.296 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEKeyEvent(event)); 1.297 + } 1.298 + } 1.299 + 1.300 + void poll() { 1.301 + if (DEBUG) { 1.302 + ThreadUtils.assertOnGeckoThread(); 1.303 + } 1.304 + if (mActions.isEmpty()) { 1.305 + throw new IllegalStateException("empty actions queue"); 1.306 + } 1.307 + mActions.poll(); 1.308 + // Don't bother locking if queue is not empty yet 1.309 + if (mActions.isEmpty()) { 1.310 + synchronized(this) { 1.311 + if (mActions.isEmpty()) { 1.312 + mActionsActive.release(); 1.313 + } 1.314 + } 1.315 + } 1.316 + } 1.317 + 1.318 + Action peek() { 1.319 + if (DEBUG) { 1.320 + ThreadUtils.assertOnGeckoThread(); 1.321 + } 1.322 + if (mActions.isEmpty()) { 1.323 + throw new IllegalStateException("empty actions queue"); 1.324 + } 1.325 + return mActions.peek(); 1.326 + } 1.327 + 1.328 + void syncWithGecko() { 1.329 + if (DEBUG) { 1.330 + assertOnIcThread(); 1.331 + } 1.332 + if (mFocused && !mActions.isEmpty()) { 1.333 + if (DEBUG) { 1.334 + Log.d(LOGTAG, "syncWithGecko blocking on thread " + 1.335 + Thread.currentThread().getName()); 1.336 + } 1.337 + mActionsActive.acquireUninterruptibly(); 1.338 + mActionsActive.release(); 1.339 + } else if (DEBUG && !mFocused) { 1.340 + Log.d(LOGTAG, "skipped syncWithGecko (no focus)"); 1.341 + } 1.342 + } 1.343 + 1.344 + boolean isEmpty() { 1.345 + return mActions.isEmpty(); 1.346 + } 1.347 + } 1.348 + 1.349 + GeckoEditable() { 1.350 + mActionQueue = new ActionQueue(); 1.351 + mSavedSelectionStart = -1; 1.352 + mUpdateGecko = true; 1.353 + 1.354 + mText = new SpannableStringBuilder(); 1.355 + mChangedText = new SpannableStringBuilder(); 1.356 + 1.357 + final Class<?>[] PROXY_INTERFACES = { Editable.class }; 1.358 + mProxy = (Editable)Proxy.newProxyInstance( 1.359 + Editable.class.getClassLoader(), 1.360 + PROXY_INTERFACES, this); 1.361 + 1.362 + LayerView v = GeckoAppShell.getLayerView(); 1.363 + mListener = GeckoInputConnection.create(v, this); 1.364 + 1.365 + mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler(); 1.366 + } 1.367 + 1.368 + private boolean onIcThread() { 1.369 + return mIcRunHandler.getLooper() == Looper.myLooper(); 1.370 + } 1.371 + 1.372 + private void assertOnIcThread() { 1.373 + ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW); 1.374 + } 1.375 + 1.376 + private void geckoPostToIc(Runnable runnable) { 1.377 + mIcPostHandler.post(runnable); 1.378 + } 1.379 + 1.380 + private void geckoUpdateGecko(final boolean force) { 1.381 + /* We do not increment the seqno here, but only check it, because geckoUpdateGecko is a 1.382 + request for update. If we incremented the seqno here, geckoUpdateGecko would have 1.383 + prevented other updates from occurring */ 1.384 + final int seqnoWhenPosted = mGeckoUpdateSeqno; 1.385 + 1.386 + geckoPostToIc(new Runnable() { 1.387 + @Override 1.388 + public void run() { 1.389 + mActionQueue.syncWithGecko(); 1.390 + if (seqnoWhenPosted == mGeckoUpdateSeqno) { 1.391 + icUpdateGecko(force); 1.392 + } 1.393 + } 1.394 + }); 1.395 + } 1.396 + 1.397 + private Object getField(Object obj, String field, Object def) { 1.398 + try { 1.399 + return obj.getClass().getField(field).get(obj); 1.400 + } catch (Exception e) { 1.401 + return def; 1.402 + } 1.403 + } 1.404 + 1.405 + private void icUpdateGecko(boolean force) { 1.406 + 1.407 + // Skip if receiving a repeated request, or 1.408 + // if suppressing compositions during text selection. 1.409 + if ((!force && mIcUpdateSeqno == mLastIcUpdateSeqno) || 1.410 + mSuppressCompositions) { 1.411 + if (DEBUG) { 1.412 + Log.d(LOGTAG, "icUpdateGecko() skipped"); 1.413 + } 1.414 + return; 1.415 + } 1.416 + mLastIcUpdateSeqno = mIcUpdateSeqno; 1.417 + mActionQueue.syncWithGecko(); 1.418 + 1.419 + if (DEBUG) { 1.420 + Log.d(LOGTAG, "icUpdateGecko()"); 1.421 + } 1.422 + 1.423 + final int selStart = mText.getSpanStart(Selection.SELECTION_START); 1.424 + final int selEnd = mText.getSpanEnd(Selection.SELECTION_END); 1.425 + int composingStart = mText.length(); 1.426 + int composingEnd = 0; 1.427 + Object[] spans = mText.getSpans(0, composingStart, Object.class); 1.428 + 1.429 + for (Object span : spans) { 1.430 + if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { 1.431 + composingStart = Math.min(composingStart, mText.getSpanStart(span)); 1.432 + composingEnd = Math.max(composingEnd, mText.getSpanEnd(span)); 1.433 + } 1.434 + } 1.435 + if (DEBUG) { 1.436 + Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd); 1.437 + Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd); 1.438 + } 1.439 + if (composingStart >= composingEnd) { 1.440 + if (selStart >= 0 && selEnd >= 0) { 1.441 + GeckoAppShell.sendEventToGecko( 1.442 + GeckoEvent.createIMESelectEvent(selStart, selEnd)); 1.443 + } else { 1.444 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( 1.445 + GeckoEvent.ImeAction.IME_REMOVE_COMPOSITION)); 1.446 + } 1.447 + return; 1.448 + } 1.449 + 1.450 + if (selEnd >= composingStart && selEnd <= composingEnd) { 1.451 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMERangeEvent( 1.452 + selEnd - composingStart, selEnd - composingStart, 1.453 + GeckoEvent.IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0)); 1.454 + } 1.455 + int rangeStart = composingStart; 1.456 + TextPaint tp = new TextPaint(); 1.457 + TextPaint emptyTp = new TextPaint(); 1.458 + // set initial foreground color to 0, because we check for tp.getColor() == 0 1.459 + // below to decide whether to pass a foreground color to Gecko 1.460 + emptyTp.setColor(0); 1.461 + do { 1.462 + int rangeType, rangeStyles = 0, rangeLineStyle = GeckoEvent.IME_RANGE_LINE_NONE; 1.463 + boolean rangeBoldLine = false; 1.464 + int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0; 1.465 + int rangeEnd = mText.nextSpanTransition(rangeStart, composingEnd, Object.class); 1.466 + 1.467 + if (selStart > rangeStart && selStart < rangeEnd) { 1.468 + rangeEnd = selStart; 1.469 + } else if (selEnd > rangeStart && selEnd < rangeEnd) { 1.470 + rangeEnd = selEnd; 1.471 + } 1.472 + CharacterStyle[] styleSpans = 1.473 + mText.getSpans(rangeStart, rangeEnd, CharacterStyle.class); 1.474 + 1.475 + if (DEBUG) { 1.476 + Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " + 1.477 + rangeStart + "-" + rangeEnd); 1.478 + } 1.479 + 1.480 + if (styleSpans.length == 0) { 1.481 + rangeType = (selStart == rangeStart && selEnd == rangeEnd) 1.482 + ? GeckoEvent.IME_RANGE_SELECTEDRAWTEXT 1.483 + : GeckoEvent.IME_RANGE_RAWINPUT; 1.484 + } else { 1.485 + rangeType = (selStart == rangeStart && selEnd == rangeEnd) 1.486 + ? GeckoEvent.IME_RANGE_SELECTEDCONVERTEDTEXT 1.487 + : GeckoEvent.IME_RANGE_CONVERTEDTEXT; 1.488 + tp.set(emptyTp); 1.489 + for (CharacterStyle span : styleSpans) { 1.490 + span.updateDrawState(tp); 1.491 + } 1.492 + int tpUnderlineColor = 0; 1.493 + float tpUnderlineThickness = 0.0f; 1.494 + // These TextPaint fields only exist on Android ICS+ and are not in the SDK 1.495 + if (Build.VERSION.SDK_INT >= 14) { 1.496 + tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0); 1.497 + tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f); 1.498 + } 1.499 + if (tpUnderlineColor != 0) { 1.500 + rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE | GeckoEvent.IME_RANGE_LINECOLOR; 1.501 + rangeLineColor = tpUnderlineColor; 1.502 + // Approximately translate underline thickness to what Gecko understands 1.503 + if (tpUnderlineThickness <= 0.5f) { 1.504 + rangeLineStyle = GeckoEvent.IME_RANGE_LINE_DOTTED; 1.505 + } else { 1.506 + rangeLineStyle = GeckoEvent.IME_RANGE_LINE_SOLID; 1.507 + if (tpUnderlineThickness >= 2.0f) { 1.508 + rangeBoldLine = true; 1.509 + } 1.510 + } 1.511 + } else if (tp.isUnderlineText()) { 1.512 + rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE; 1.513 + rangeLineStyle = GeckoEvent.IME_RANGE_LINE_SOLID; 1.514 + } 1.515 + if (tp.getColor() != 0) { 1.516 + rangeStyles |= GeckoEvent.IME_RANGE_FORECOLOR; 1.517 + rangeForeColor = tp.getColor(); 1.518 + } 1.519 + if (tp.bgColor != 0) { 1.520 + rangeStyles |= GeckoEvent.IME_RANGE_BACKCOLOR; 1.521 + rangeBackColor = tp.bgColor; 1.522 + } 1.523 + } 1.524 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMERangeEvent( 1.525 + rangeStart - composingStart, rangeEnd - composingStart, 1.526 + rangeType, rangeStyles, rangeLineStyle, rangeBoldLine, 1.527 + rangeForeColor, rangeBackColor, rangeLineColor)); 1.528 + rangeStart = rangeEnd; 1.529 + 1.530 + if (DEBUG) { 1.531 + Log.d(LOGTAG, " added " + rangeType + 1.532 + " : " + Integer.toHexString(rangeStyles) + 1.533 + " : " + Integer.toHexString(rangeForeColor) + 1.534 + " : " + Integer.toHexString(rangeBackColor)); 1.535 + } 1.536 + } while (rangeStart < composingEnd); 1.537 + 1.538 + GeckoAppShell.sendEventToGecko(GeckoEvent.createIMECompositionEvent( 1.539 + composingStart, composingEnd)); 1.540 + } 1.541 + 1.542 + // GeckoEditableClient interface 1.543 + 1.544 + @Override 1.545 + public void sendEvent(final GeckoEvent event) { 1.546 + if (DEBUG) { 1.547 + assertOnIcThread(); 1.548 + Log.d(LOGTAG, "sendEvent(" + event + ")"); 1.549 + } 1.550 + /* 1.551 + We are actually sending two events to Gecko here, 1.552 + 1. Event from the event parameter (key event, etc.) 1.553 + 2. Sync event from the mActionQueue.offer call 1.554 + The first event is a normal GeckoEvent that does not reply back to us, 1.555 + the second sync event will have a reply, during which we see that there is a pending 1.556 + event-type action, and update the selection/composition/etc. accordingly. 1.557 + */ 1.558 + GeckoAppShell.sendEventToGecko(event); 1.559 + mActionQueue.offer(new Action(Action.TYPE_EVENT)); 1.560 + } 1.561 + 1.562 + @Override 1.563 + public Editable getEditable() { 1.564 + if (!onIcThread()) { 1.565 + // Android may be holding an old InputConnection; ignore 1.566 + if (DEBUG) { 1.567 + Log.i(LOGTAG, "getEditable() called on non-IC thread"); 1.568 + } 1.569 + return null; 1.570 + } 1.571 + return mProxy; 1.572 + } 1.573 + 1.574 + @Override 1.575 + public void setUpdateGecko(boolean update) { 1.576 + if (!onIcThread()) { 1.577 + // Android may be holding an old InputConnection; ignore 1.578 + if (DEBUG) { 1.579 + Log.i(LOGTAG, "setUpdateGecko() called on non-IC thread"); 1.580 + } 1.581 + return; 1.582 + } 1.583 + if (update) { 1.584 + icUpdateGecko(false); 1.585 + } 1.586 + mUpdateGecko = update; 1.587 + } 1.588 + 1.589 + @Override 1.590 + public void setSuppressKeyUp(boolean suppress) { 1.591 + if (DEBUG) { 1.592 + // only used by key event handler 1.593 + ThreadUtils.assertOnUiThread(); 1.594 + } 1.595 + // Suppress key up event generated as a result of 1.596 + // translating characters to key events 1.597 + mSuppressKeyUp = suppress; 1.598 + } 1.599 + 1.600 + @Override 1.601 + public Handler getInputConnectionHandler() { 1.602 + // Can be called from either UI thread or IC thread; 1.603 + // care must be taken to avoid race conditions 1.604 + return mIcRunHandler; 1.605 + } 1.606 + 1.607 + @Override 1.608 + public boolean setInputConnectionHandler(Handler handler) { 1.609 + if (handler == mIcPostHandler) { 1.610 + return true; 1.611 + } 1.612 + if (!mFocused) { 1.613 + return false; 1.614 + } 1.615 + if (DEBUG) { 1.616 + assertOnIcThread(); 1.617 + } 1.618 + // There are three threads at this point: Gecko thread, old IC thread, and new IC 1.619 + // thread, and we want to safely switch from old IC thread to new IC thread. 1.620 + // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that 1.621 + // the Gecko thread is stopped at a known point. At the same time, the old IC 1.622 + // thread blocks on the action; this ensures that the old IC thread is stopped at 1.623 + // a known point. Finally, inside the Gecko thread, we post a Runnable to the old 1.624 + // IC thread; this Runnable switches from old IC thread to new IC thread. We 1.625 + // switch IC thread on the old IC thread to ensure any pending Runnables on the 1.626 + // old IC thread are processed before we switch over. Inside the Gecko thread, we 1.627 + // also post a Runnable to the new IC thread; this Runnable blocks until the 1.628 + // switch is complete; this ensures that the new IC thread won't accept 1.629 + // InputConnection calls until after the switch. 1.630 + mActionQueue.offer(Action.newSetHandler(handler)); 1.631 + mActionQueue.syncWithGecko(); 1.632 + return true; 1.633 + } 1.634 + 1.635 + private void geckoSetIcHandler(final Handler newHandler) { 1.636 + geckoPostToIc(new Runnable() { // posting to old IC thread 1.637 + @Override 1.638 + public void run() { 1.639 + synchronized (newHandler) { 1.640 + mIcRunHandler = newHandler; 1.641 + newHandler.notify(); 1.642 + } 1.643 + } 1.644 + }); 1.645 + 1.646 + // At this point, all future Runnables should be posted to the new IC thread, but 1.647 + // we don't switch mIcRunHandler yet because there may be pending Runnables on the 1.648 + // old IC thread still waiting to run. 1.649 + mIcPostHandler = newHandler; 1.650 + 1.651 + geckoPostToIc(new Runnable() { // posting to new IC thread 1.652 + @Override 1.653 + public void run() { 1.654 + synchronized (newHandler) { 1.655 + while (mIcRunHandler != newHandler) { 1.656 + try { 1.657 + newHandler.wait(); 1.658 + } catch (InterruptedException e) { 1.659 + } 1.660 + } 1.661 + } 1.662 + } 1.663 + }); 1.664 + } 1.665 + 1.666 + // GeckoEditableListener interface 1.667 + 1.668 + private void geckoActionReply() { 1.669 + if (DEBUG) { 1.670 + // GeckoEditableListener methods should all be called from the Gecko thread 1.671 + ThreadUtils.assertOnGeckoThread(); 1.672 + } 1.673 + final Action action = mActionQueue.peek(); 1.674 + 1.675 + if (DEBUG) { 1.676 + Log.d(LOGTAG, "reply: Action(" + 1.677 + getConstantName(Action.class, "TYPE_", action.mType) + ")"); 1.678 + } 1.679 + switch (action.mType) { 1.680 + case Action.TYPE_SET_SELECTION: 1.681 + final int len = mText.length(); 1.682 + final int curStart = Selection.getSelectionStart(mText); 1.683 + final int curEnd = Selection.getSelectionEnd(mText); 1.684 + // start == -1 when the start offset should remain the same 1.685 + // end == -1 when the end offset should remain the same 1.686 + final int selStart = Math.min(action.mStart < 0 ? curStart : action.mStart, len); 1.687 + final int selEnd = Math.min(action.mEnd < 0 ? curEnd : action.mEnd, len); 1.688 + 1.689 + if (selStart < action.mStart || selEnd < action.mEnd) { 1.690 + Log.w(LOGTAG, "IME sync error: selection out of bounds"); 1.691 + } 1.692 + Selection.setSelection(mText, selStart, selEnd); 1.693 + geckoPostToIc(new Runnable() { 1.694 + @Override 1.695 + public void run() { 1.696 + mActionQueue.syncWithGecko(); 1.697 + final int start = Selection.getSelectionStart(mText); 1.698 + final int end = Selection.getSelectionEnd(mText); 1.699 + if (selStart == start && selEnd == end) { 1.700 + // There has not been another new selection in the mean time that 1.701 + // made this notification out-of-date 1.702 + mListener.onSelectionChange(start, end); 1.703 + } 1.704 + } 1.705 + }); 1.706 + break; 1.707 + case Action.TYPE_SET_SPAN: 1.708 + mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags); 1.709 + break; 1.710 + case Action.TYPE_SET_HANDLER: 1.711 + geckoSetIcHandler(action.mHandler); 1.712 + break; 1.713 + } 1.714 + if (action.mShouldUpdate) { 1.715 + geckoUpdateGecko(false); 1.716 + } 1.717 + } 1.718 + 1.719 + @Override 1.720 + public void notifyIME(final int type) { 1.721 + if (DEBUG) { 1.722 + // GeckoEditableListener methods should all be called from the Gecko thread 1.723 + ThreadUtils.assertOnGeckoThread(); 1.724 + // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply() 1.725 + if (type != NOTIFY_IME_REPLY_EVENT) { 1.726 + Log.d(LOGTAG, "notifyIME(" + 1.727 + getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) + 1.728 + ")"); 1.729 + } 1.730 + } 1.731 + if (type == NOTIFY_IME_REPLY_EVENT) { 1.732 + try { 1.733 + if (mFocused) { 1.734 + // When mFocused is false, the reply is for a stale action, 1.735 + // and we should not do anything 1.736 + geckoActionReply(); 1.737 + } else if (DEBUG) { 1.738 + Log.d(LOGTAG, "discarding stale reply"); 1.739 + } 1.740 + } finally { 1.741 + // Ensure action is always removed from queue 1.742 + // even if stale action results in exception in geckoActionReply 1.743 + mActionQueue.poll(); 1.744 + } 1.745 + return; 1.746 + } 1.747 + geckoPostToIc(new Runnable() { 1.748 + @Override 1.749 + public void run() { 1.750 + if (type == NOTIFY_IME_OF_BLUR) { 1.751 + mFocused = false; 1.752 + } else if (type == NOTIFY_IME_OF_FOCUS) { 1.753 + mFocused = true; 1.754 + // Unmask events on the Gecko side 1.755 + mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS)); 1.756 + } 1.757 + // Make sure there are no other things going on. If we sent 1.758 + // GeckoEvent.IME_ACKNOWLEDGE_FOCUS, this line also makes us 1.759 + // wait for Gecko to update us on the newly focused content 1.760 + mActionQueue.syncWithGecko(); 1.761 + mListener.notifyIME(type); 1.762 + } 1.763 + }); 1.764 + 1.765 + // Register/unregister Gecko-side text selection listeners 1.766 + // and update the mGeckoFocused flag. 1.767 + if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) { 1.768 + // Check for focus here because Gecko may send us a blur before a focus in some 1.769 + // cases, and we don't want to unregister an event that was not registered. 1.770 + mGeckoFocused = false; 1.771 + mSuppressCompositions = false; 1.772 + GeckoAppShell.getEventDispatcher(). 1.773 + unregisterEventListener("TextSelection:DraggingHandle", this); 1.774 + } else if (type == NOTIFY_IME_OF_FOCUS) { 1.775 + mGeckoFocused = true; 1.776 + mSuppressCompositions = false; 1.777 + GeckoAppShell.getEventDispatcher(). 1.778 + registerEventListener("TextSelection:DraggingHandle", this); 1.779 + } 1.780 + } 1.781 + 1.782 + @Override 1.783 + public void notifyIMEContext(final int state, final String typeHint, 1.784 + final String modeHint, final String actionHint) { 1.785 + // Because we want to be able to bind GeckoEditable to the newest LayerView instance, 1.786 + // this can be called from the Java IC thread in addition to the Gecko thread. 1.787 + if (DEBUG) { 1.788 + Log.d(LOGTAG, "notifyIMEContext(" + 1.789 + getConstantName(GeckoEditableListener.class, "IME_STATE_", state) + 1.790 + ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")"); 1.791 + } 1.792 + geckoPostToIc(new Runnable() { 1.793 + @Override 1.794 + public void run() { 1.795 + // Make sure there are no other things going on 1.796 + mActionQueue.syncWithGecko(); 1.797 + // Set InputConnectionHandler in notifyIMEContext because 1.798 + // GeckoInputConnection.notifyIMEContext calls restartInput() which will invoke 1.799 + // InputConnectionHandler.onCreateInputConnection 1.800 + LayerView v = GeckoAppShell.getLayerView(); 1.801 + if (v != null) { 1.802 + mListener = GeckoInputConnection.create(v, GeckoEditable.this); 1.803 + v.setInputConnectionHandler((InputConnectionHandler)mListener); 1.804 + mListener.notifyIMEContext(state, typeHint, modeHint, actionHint); 1.805 + } 1.806 + } 1.807 + }); 1.808 + } 1.809 + 1.810 + @Override 1.811 + public void onSelectionChange(final int start, final int end) { 1.812 + if (DEBUG) { 1.813 + // GeckoEditableListener methods should all be called from the Gecko thread 1.814 + ThreadUtils.assertOnGeckoThread(); 1.815 + Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")"); 1.816 + } 1.817 + if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) { 1.818 + throw new IllegalArgumentException("invalid selection notification range: " + 1.819 + start + " to " + end + ", length: " + mText.length()); 1.820 + } 1.821 + final int seqnoWhenPosted = ++mGeckoUpdateSeqno; 1.822 + 1.823 + /* An event (keypress, etc.) has potentially changed the selection, 1.824 + synchronize the selection here. There is not a race with the IC thread 1.825 + because the IC thread should be blocked on the event action */ 1.826 + if (!mActionQueue.isEmpty() && 1.827 + mActionQueue.peek().mType == Action.TYPE_EVENT) { 1.828 + Selection.setSelection(mText, start, end); 1.829 + return; 1.830 + } 1.831 + 1.832 + geckoPostToIc(new Runnable() { 1.833 + @Override 1.834 + public void run() { 1.835 + mActionQueue.syncWithGecko(); 1.836 + /* check to see there has not been another action that potentially changed the 1.837 + selection. If so, we can skip this update because we know there is another 1.838 + update right after this one that will replace the effect of this update */ 1.839 + if (mGeckoUpdateSeqno == seqnoWhenPosted) { 1.840 + /* In this case, Gecko's selection has changed and it's notifying us to change 1.841 + Java's selection. In the normal case, whenever Java's selection changes, 1.842 + we go back and set Gecko's selection as well. However, in this case, 1.843 + since Gecko's selection is already up-to-date, we skip this step. */ 1.844 + boolean oldUpdateGecko = mUpdateGecko; 1.845 + mUpdateGecko = false; 1.846 + Selection.setSelection(mProxy, start, end); 1.847 + mUpdateGecko = oldUpdateGecko; 1.848 + } 1.849 + } 1.850 + }); 1.851 + } 1.852 + 1.853 + private void geckoReplaceText(int start, int oldEnd, CharSequence newText) { 1.854 + // Don't use replace() because Gingerbread has a bug where if the replaced text 1.855 + // has the same spans as the original text, the spans will end up being deleted 1.856 + mText.delete(start, oldEnd); 1.857 + mText.insert(start, newText); 1.858 + } 1.859 + 1.860 + @Override 1.861 + public void onTextChange(final String text, final int start, 1.862 + final int unboundedOldEnd, final int unboundedNewEnd) { 1.863 + if (DEBUG) { 1.864 + // GeckoEditableListener methods should all be called from the Gecko thread 1.865 + ThreadUtils.assertOnGeckoThread(); 1.866 + StringBuilder sb = new StringBuilder("onTextChange("); 1.867 + debugAppend(sb, text); 1.868 + sb.append(", ").append(start).append(", ") 1.869 + .append(unboundedOldEnd).append(", ") 1.870 + .append(unboundedNewEnd).append(")"); 1.871 + Log.d(LOGTAG, sb.toString()); 1.872 + } 1.873 + if (start < 0 || start > unboundedOldEnd) { 1.874 + throw new IllegalArgumentException("invalid text notification range: " + 1.875 + start + " to " + unboundedOldEnd); 1.876 + } 1.877 + /* For the "end" parameters, Gecko can pass in a large 1.878 + number to denote "end of the text". Fix that here */ 1.879 + final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd; 1.880 + // new end should always match text 1.881 + if (start != 0 && unboundedNewEnd != (start + text.length())) { 1.882 + throw new IllegalArgumentException("newEnd does not match text: " + 1.883 + unboundedNewEnd + " vs " + (start + text.length())); 1.884 + } 1.885 + final int newEnd = start + text.length(); 1.886 + 1.887 + /* Text changes affect the selection as well, and we may not receive another selection 1.888 + update as a result of selection notification masking on the Gecko side; therefore, 1.889 + in order to prevent previous stale selection notifications from occurring, we need 1.890 + to increment the seqno here as well */ 1.891 + ++mGeckoUpdateSeqno; 1.892 + 1.893 + mChangedText.clearSpans(); 1.894 + mChangedText.replace(0, mChangedText.length(), text); 1.895 + // Preserve as many spans as possible 1.896 + TextUtils.copySpansFrom(mText, start, Math.min(oldEnd, newEnd), 1.897 + Object.class, mChangedText, 0); 1.898 + 1.899 + if (!mActionQueue.isEmpty()) { 1.900 + final Action action = mActionQueue.peek(); 1.901 + if (action.mType == Action.TYPE_REPLACE_TEXT && 1.902 + start <= action.mStart && 1.903 + action.mStart + action.mSequence.length() <= newEnd) { 1.904 + 1.905 + // actionNewEnd is the new end of the original replacement action 1.906 + final int actionNewEnd = action.mStart + action.mSequence.length(); 1.907 + int selStart = Selection.getSelectionStart(mText); 1.908 + int selEnd = Selection.getSelectionEnd(mText); 1.909 + 1.910 + // Replace old spans with new spans 1.911 + mChangedText.replace(action.mStart - start, actionNewEnd - start, 1.912 + action.mSequence); 1.913 + geckoReplaceText(start, oldEnd, mChangedText); 1.914 + 1.915 + // delete/insert above might have moved our selection to somewhere else 1.916 + // this happens when the Gecko text change covers a larger range than 1.917 + // the original replacement action. Fix selection here 1.918 + if (selStart >= start && selStart <= oldEnd) { 1.919 + selStart = selStart < action.mStart ? selStart : 1.920 + selStart < action.mEnd ? actionNewEnd : 1.921 + selStart + actionNewEnd - action.mEnd; 1.922 + mText.setSpan(Selection.SELECTION_START, selStart, selStart, 1.923 + Spanned.SPAN_POINT_POINT); 1.924 + } 1.925 + if (selEnd >= start && selEnd <= oldEnd) { 1.926 + selEnd = selEnd < action.mStart ? selEnd : 1.927 + selEnd < action.mEnd ? actionNewEnd : 1.928 + selEnd + actionNewEnd - action.mEnd; 1.929 + mText.setSpan(Selection.SELECTION_END, selEnd, selEnd, 1.930 + Spanned.SPAN_POINT_POINT); 1.931 + } 1.932 + } else { 1.933 + geckoReplaceText(start, oldEnd, mChangedText); 1.934 + } 1.935 + } else { 1.936 + geckoReplaceText(start, oldEnd, mChangedText); 1.937 + } 1.938 + geckoPostToIc(new Runnable() { 1.939 + @Override 1.940 + public void run() { 1.941 + mListener.onTextChange(text, start, oldEnd, newEnd); 1.942 + } 1.943 + }); 1.944 + } 1.945 + 1.946 + // InvocationHandler interface 1.947 + 1.948 + static String getConstantName(Class<?> cls, String prefix, Object value) { 1.949 + for (Field fld : cls.getDeclaredFields()) { 1.950 + try { 1.951 + if (fld.getName().startsWith(prefix) && 1.952 + fld.get(null).equals(value)) { 1.953 + return fld.getName(); 1.954 + } 1.955 + } catch (IllegalAccessException e) { 1.956 + } 1.957 + } 1.958 + return String.valueOf(value); 1.959 + } 1.960 + 1.961 + static StringBuilder debugAppend(StringBuilder sb, Object obj) { 1.962 + if (obj == null) { 1.963 + sb.append("null"); 1.964 + } else if (obj instanceof GeckoEditable) { 1.965 + sb.append("GeckoEditable"); 1.966 + } else if (Proxy.isProxyClass(obj.getClass())) { 1.967 + debugAppend(sb, Proxy.getInvocationHandler(obj)); 1.968 + } else if (obj instanceof CharSequence) { 1.969 + sb.append("\"").append(obj.toString().replace('\n', '\u21b2')).append("\""); 1.970 + } else if (obj.getClass().isArray()) { 1.971 + sb.append(obj.getClass().getComponentType().getSimpleName()).append("[") 1.972 + .append(java.lang.reflect.Array.getLength(obj)).append("]"); 1.973 + } else { 1.974 + sb.append(obj.toString()); 1.975 + } 1.976 + return sb; 1.977 + } 1.978 + 1.979 + @Override 1.980 + public Object invoke(Object proxy, Method method, Object[] args) 1.981 + throws Throwable { 1.982 + Object target; 1.983 + final Class<?> methodInterface = method.getDeclaringClass(); 1.984 + if (DEBUG) { 1.985 + // Editable methods should all be called from the IC thread 1.986 + assertOnIcThread(); 1.987 + } 1.988 + if (methodInterface == Editable.class || 1.989 + methodInterface == Appendable.class || 1.990 + methodInterface == Spannable.class) { 1.991 + // Method alters the Editable; route calls to our implementation 1.992 + target = this; 1.993 + } else { 1.994 + // Method queries the Editable; must sync with Gecko first 1.995 + // then call on the inner Editable itself 1.996 + mActionQueue.syncWithGecko(); 1.997 + target = mText; 1.998 + } 1.999 + Object ret; 1.1000 + try { 1.1001 + ret = method.invoke(target, args); 1.1002 + } catch (InvocationTargetException e) { 1.1003 + // Bug 817386 1.1004 + // Most likely Gecko has changed the text while GeckoInputConnection is 1.1005 + // trying to access the text. If we pass through the exception here, Fennec 1.1006 + // will crash due to a lack of exception handler. Log the exception and 1.1007 + // return an empty value instead. 1.1008 + if (!(e.getCause() instanceof IndexOutOfBoundsException)) { 1.1009 + // Only handle IndexOutOfBoundsException for now, 1.1010 + // as other exceptions might signal other bugs 1.1011 + throw e; 1.1012 + } 1.1013 + Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause()); 1.1014 + Class<?> retClass = method.getReturnType(); 1.1015 + if (retClass == Character.TYPE) { 1.1016 + ret = '\0'; 1.1017 + } else if (retClass == Integer.TYPE) { 1.1018 + ret = 0; 1.1019 + } else if (retClass == String.class) { 1.1020 + ret = ""; 1.1021 + } else { 1.1022 + ret = null; 1.1023 + } 1.1024 + } 1.1025 + if (DEBUG) { 1.1026 + StringBuilder log = new StringBuilder(method.getName()); 1.1027 + log.append("("); 1.1028 + for (Object arg : args) { 1.1029 + debugAppend(log, arg).append(", "); 1.1030 + } 1.1031 + if (args.length > 0) { 1.1032 + log.setLength(log.length() - 2); 1.1033 + } 1.1034 + if (method.getReturnType().equals(Void.TYPE)) { 1.1035 + log.append(")"); 1.1036 + } else { 1.1037 + debugAppend(log.append(") = "), ret); 1.1038 + } 1.1039 + Log.d(LOGTAG, log.toString()); 1.1040 + } 1.1041 + return ret; 1.1042 + } 1.1043 + 1.1044 + // Spannable interface 1.1045 + 1.1046 + @Override 1.1047 + public void removeSpan(Object what) { 1.1048 + if (what == Selection.SELECTION_START || 1.1049 + what == Selection.SELECTION_END) { 1.1050 + Log.w(LOGTAG, "selection removed with removeSpan()"); 1.1051 + } 1.1052 + if (mText.getSpanStart(what) >= 0) { // only remove if it's there 1.1053 + // Okay to remove immediately 1.1054 + mText.removeSpan(what); 1.1055 + mActionQueue.offer(new Action(Action.TYPE_REMOVE_SPAN)); 1.1056 + } 1.1057 + } 1.1058 + 1.1059 + @Override 1.1060 + public void setSpan(Object what, int start, int end, int flags) { 1.1061 + if (what == Selection.SELECTION_START) { 1.1062 + if ((flags & Spanned.SPAN_INTERMEDIATE) != 0) { 1.1063 + // We will get the end offset next, just save the start for now 1.1064 + mSavedSelectionStart = start; 1.1065 + } else { 1.1066 + mActionQueue.offer(Action.newSetSelection(start, -1)); 1.1067 + } 1.1068 + } else if (what == Selection.SELECTION_END) { 1.1069 + mActionQueue.offer(Action.newSetSelection(mSavedSelectionStart, end)); 1.1070 + mSavedSelectionStart = -1; 1.1071 + } else { 1.1072 + mActionQueue.offer(Action.newSetSpan(what, start, end, flags)); 1.1073 + } 1.1074 + } 1.1075 + 1.1076 + // Appendable interface 1.1077 + 1.1078 + @Override 1.1079 + public Editable append(CharSequence text) { 1.1080 + return replace(mProxy.length(), mProxy.length(), text, 0, text.length()); 1.1081 + } 1.1082 + 1.1083 + @Override 1.1084 + public Editable append(CharSequence text, int start, int end) { 1.1085 + return replace(mProxy.length(), mProxy.length(), text, start, end); 1.1086 + } 1.1087 + 1.1088 + @Override 1.1089 + public Editable append(char text) { 1.1090 + return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1); 1.1091 + } 1.1092 + 1.1093 + // Editable interface 1.1094 + 1.1095 + @Override 1.1096 + public InputFilter[] getFilters() { 1.1097 + return mFilters; 1.1098 + } 1.1099 + 1.1100 + @Override 1.1101 + public void setFilters(InputFilter[] filters) { 1.1102 + mFilters = filters; 1.1103 + } 1.1104 + 1.1105 + @Override 1.1106 + public void clearSpans() { 1.1107 + /* XXX this clears the selection spans too, 1.1108 + but there is no way to clear the corresponding selection in Gecko */ 1.1109 + Log.w(LOGTAG, "selection cleared with clearSpans()"); 1.1110 + mText.clearSpans(); 1.1111 + } 1.1112 + 1.1113 + @Override 1.1114 + public Editable replace(int st, int en, 1.1115 + CharSequence source, int start, int end) { 1.1116 + 1.1117 + CharSequence text = source; 1.1118 + if (start < 0 || start > end || end > text.length()) { 1.1119 + throw new IllegalArgumentException("invalid replace offsets: " + 1.1120 + start + " to " + end + ", length: " + text.length()); 1.1121 + } 1.1122 + if (start != 0 || end != text.length()) { 1.1123 + text = text.subSequence(start, end); 1.1124 + } 1.1125 + if (mFilters != null) { 1.1126 + // Filter text before sending the request to Gecko 1.1127 + for (int i = 0; i < mFilters.length; ++i) { 1.1128 + final CharSequence cs = mFilters[i].filter( 1.1129 + text, 0, text.length(), mProxy, st, en); 1.1130 + if (cs != null) { 1.1131 + text = cs; 1.1132 + } 1.1133 + } 1.1134 + } 1.1135 + if (text == source) { 1.1136 + // Always create a copy 1.1137 + text = new SpannableString(source); 1.1138 + } 1.1139 + mActionQueue.offer(Action.newReplaceText(text, 1.1140 + Math.min(st, en), Math.max(st, en))); 1.1141 + return mProxy; 1.1142 + } 1.1143 + 1.1144 + @Override 1.1145 + public void clear() { 1.1146 + replace(0, mProxy.length(), "", 0, 0); 1.1147 + } 1.1148 + 1.1149 + @Override 1.1150 + public Editable delete(int st, int en) { 1.1151 + return replace(st, en, "", 0, 0); 1.1152 + } 1.1153 + 1.1154 + @Override 1.1155 + public Editable insert(int where, CharSequence text, 1.1156 + int start, int end) { 1.1157 + return replace(where, where, text, start, end); 1.1158 + } 1.1159 + 1.1160 + @Override 1.1161 + public Editable insert(int where, CharSequence text) { 1.1162 + return replace(where, where, text, 0, text.length()); 1.1163 + } 1.1164 + 1.1165 + @Override 1.1166 + public Editable replace(int st, int en, CharSequence text) { 1.1167 + return replace(st, en, text, 0, text.length()); 1.1168 + } 1.1169 + 1.1170 + /* GetChars interface */ 1.1171 + 1.1172 + @Override 1.1173 + public void getChars(int start, int end, char[] dest, int destoff) { 1.1174 + /* overridden Editable interface methods in GeckoEditable must not be called directly 1.1175 + outside of GeckoEditable. Instead, the call must go through mProxy, which ensures 1.1176 + that Java is properly synchronized with Gecko */ 1.1177 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1178 + } 1.1179 + 1.1180 + /* Spanned interface */ 1.1181 + 1.1182 + @Override 1.1183 + public int getSpanEnd(Object tag) { 1.1184 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1185 + } 1.1186 + 1.1187 + @Override 1.1188 + public int getSpanFlags(Object tag) { 1.1189 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1190 + } 1.1191 + 1.1192 + @Override 1.1193 + public int getSpanStart(Object tag) { 1.1194 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1195 + } 1.1196 + 1.1197 + @Override 1.1198 + public <T> T[] getSpans(int start, int end, Class<T> type) { 1.1199 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1200 + } 1.1201 + 1.1202 + @Override 1.1203 + @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration 1.1204 + public int nextSpanTransition(int start, int limit, Class type) { 1.1205 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1206 + } 1.1207 + 1.1208 + /* CharSequence interface */ 1.1209 + 1.1210 + @Override 1.1211 + public char charAt(int index) { 1.1212 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1213 + } 1.1214 + 1.1215 + @Override 1.1216 + public int length() { 1.1217 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1218 + } 1.1219 + 1.1220 + @Override 1.1221 + public CharSequence subSequence(int start, int end) { 1.1222 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1223 + } 1.1224 + 1.1225 + @Override 1.1226 + public String toString() { 1.1227 + throw new UnsupportedOperationException("method must be called through mProxy"); 1.1228 + } 1.1229 + 1.1230 + // GeckoEventListener implementation 1.1231 + 1.1232 + @Override 1.1233 + public void handleMessage(String event, JSONObject message) { 1.1234 + if (!"TextSelection:DraggingHandle".equals(event)) { 1.1235 + return; 1.1236 + } 1.1237 + 1.1238 + mSuppressCompositions = message.optBoolean("dragging", false); 1.1239 + } 1.1240 +} 1.1241 +