Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko; |
michael@0 | 7 | |
michael@0 | 8 | import org.mozilla.gecko.gfx.InputConnectionHandler; |
michael@0 | 9 | import org.mozilla.gecko.gfx.LayerView; |
michael@0 | 10 | import org.mozilla.gecko.util.GeckoEventListener; |
michael@0 | 11 | import org.mozilla.gecko.util.ThreadUtils; |
michael@0 | 12 | import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; |
michael@0 | 13 | |
michael@0 | 14 | import org.json.JSONObject; |
michael@0 | 15 | |
michael@0 | 16 | import android.os.Build; |
michael@0 | 17 | import android.os.Handler; |
michael@0 | 18 | import android.os.Looper; |
michael@0 | 19 | import android.text.Editable; |
michael@0 | 20 | import android.text.InputFilter; |
michael@0 | 21 | import android.text.Selection; |
michael@0 | 22 | import android.text.Spannable; |
michael@0 | 23 | import android.text.SpannableString; |
michael@0 | 24 | import android.text.SpannableStringBuilder; |
michael@0 | 25 | import android.text.Spanned; |
michael@0 | 26 | import android.text.TextPaint; |
michael@0 | 27 | import android.text.TextUtils; |
michael@0 | 28 | import android.text.style.CharacterStyle; |
michael@0 | 29 | import android.util.Log; |
michael@0 | 30 | import android.view.KeyCharacterMap; |
michael@0 | 31 | import android.view.KeyEvent; |
michael@0 | 32 | |
michael@0 | 33 | import java.lang.reflect.Field; |
michael@0 | 34 | import java.lang.reflect.InvocationHandler; |
michael@0 | 35 | import java.lang.reflect.InvocationTargetException; |
michael@0 | 36 | import java.lang.reflect.Method; |
michael@0 | 37 | import java.lang.reflect.Proxy; |
michael@0 | 38 | import java.util.concurrent.ConcurrentLinkedQueue; |
michael@0 | 39 | import java.util.concurrent.Semaphore; |
michael@0 | 40 | |
michael@0 | 41 | // interface for the IC thread |
michael@0 | 42 | interface GeckoEditableClient { |
michael@0 | 43 | void sendEvent(GeckoEvent event); |
michael@0 | 44 | Editable getEditable(); |
michael@0 | 45 | void setUpdateGecko(boolean update); |
michael@0 | 46 | void setSuppressKeyUp(boolean suppress); |
michael@0 | 47 | Handler getInputConnectionHandler(); |
michael@0 | 48 | boolean setInputConnectionHandler(Handler handler); |
michael@0 | 49 | } |
michael@0 | 50 | |
michael@0 | 51 | /* interface for the Editable to listen to the Gecko thread |
michael@0 | 52 | and also for the IC thread to listen to the Editable */ |
michael@0 | 53 | interface GeckoEditableListener { |
michael@0 | 54 | // IME notification type for notifyIME(), corresponding to NotificationToIME enum in Gecko |
michael@0 | 55 | final int NOTIFY_IME_OPEN_VKB = -2; |
michael@0 | 56 | final int NOTIFY_IME_REPLY_EVENT = -1; |
michael@0 | 57 | final int NOTIFY_IME_OF_FOCUS = 1; |
michael@0 | 58 | final int NOTIFY_IME_OF_BLUR = 2; |
michael@0 | 59 | final int NOTIFY_IME_TO_COMMIT_COMPOSITION = 7; |
michael@0 | 60 | final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 8; |
michael@0 | 61 | // IME enabled state for notifyIMEContext() |
michael@0 | 62 | final int IME_STATE_DISABLED = 0; |
michael@0 | 63 | final int IME_STATE_ENABLED = 1; |
michael@0 | 64 | final int IME_STATE_PASSWORD = 2; |
michael@0 | 65 | final int IME_STATE_PLUGIN = 3; |
michael@0 | 66 | |
michael@0 | 67 | void notifyIME(int type); |
michael@0 | 68 | void notifyIMEContext(int state, String typeHint, |
michael@0 | 69 | String modeHint, String actionHint); |
michael@0 | 70 | void onSelectionChange(int start, int end); |
michael@0 | 71 | void onTextChange(String text, int start, int oldEnd, int newEnd); |
michael@0 | 72 | } |
michael@0 | 73 | |
michael@0 | 74 | /* |
michael@0 | 75 | GeckoEditable implements only some functions of Editable |
michael@0 | 76 | The field mText contains the actual underlying |
michael@0 | 77 | SpannableStringBuilder/Editable that contains our text. |
michael@0 | 78 | */ |
michael@0 | 79 | final class GeckoEditable |
michael@0 | 80 | implements InvocationHandler, Editable, |
michael@0 | 81 | GeckoEditableClient, GeckoEditableListener, GeckoEventListener { |
michael@0 | 82 | |
michael@0 | 83 | private static final boolean DEBUG = false; |
michael@0 | 84 | private static final String LOGTAG = "GeckoEditable"; |
michael@0 | 85 | |
michael@0 | 86 | // Filters to implement Editable's filtering functionality |
michael@0 | 87 | private InputFilter[] mFilters; |
michael@0 | 88 | |
michael@0 | 89 | private final SpannableStringBuilder mText; |
michael@0 | 90 | private final SpannableStringBuilder mChangedText; |
michael@0 | 91 | private final Editable mProxy; |
michael@0 | 92 | private final ActionQueue mActionQueue; |
michael@0 | 93 | |
michael@0 | 94 | // mIcRunHandler is the Handler that currently runs Gecko-to-IC Runnables |
michael@0 | 95 | // mIcPostHandler is the Handler to post Gecko-to-IC Runnables to |
michael@0 | 96 | // The two can be different when switching from one handler to another |
michael@0 | 97 | private Handler mIcRunHandler; |
michael@0 | 98 | private Handler mIcPostHandler; |
michael@0 | 99 | |
michael@0 | 100 | private GeckoEditableListener mListener; |
michael@0 | 101 | private int mSavedSelectionStart; |
michael@0 | 102 | private volatile int mGeckoUpdateSeqno; |
michael@0 | 103 | private int mIcUpdateSeqno; |
michael@0 | 104 | private int mLastIcUpdateSeqno; |
michael@0 | 105 | private boolean mUpdateGecko; |
michael@0 | 106 | private boolean mFocused; // Used by IC thread |
michael@0 | 107 | private boolean mGeckoFocused; // Used by Gecko thread |
michael@0 | 108 | private volatile boolean mSuppressCompositions; |
michael@0 | 109 | private volatile boolean mSuppressKeyUp; |
michael@0 | 110 | |
michael@0 | 111 | /* An action that alters the Editable |
michael@0 | 112 | |
michael@0 | 113 | Each action corresponds to a Gecko event. While the Gecko event is being sent to the Gecko |
michael@0 | 114 | thread, the action stays on top of mActions queue. After the Gecko event is processed and |
michael@0 | 115 | replied, the action is removed from the queue |
michael@0 | 116 | */ |
michael@0 | 117 | private static final class Action { |
michael@0 | 118 | // For input events (keypress, etc.); use with IME_SYNCHRONIZE |
michael@0 | 119 | static final int TYPE_EVENT = 0; |
michael@0 | 120 | // For Editable.replace() call; use with IME_REPLACE_TEXT |
michael@0 | 121 | static final int TYPE_REPLACE_TEXT = 1; |
michael@0 | 122 | /* For Editable.setSpan(Selection...) call; use with IME_SYNCHRONIZE |
michael@0 | 123 | Note that we don't use this with IME_SET_SELECTION because we don't want to update the |
michael@0 | 124 | Gecko selection at the point of this action. The Gecko selection is updated only after |
michael@0 | 125 | IC has updated its selection (during IME_SYNCHRONIZE reply) */ |
michael@0 | 126 | static final int TYPE_SET_SELECTION = 2; |
michael@0 | 127 | // For Editable.setSpan() call; use with IME_SYNCHRONIZE |
michael@0 | 128 | static final int TYPE_SET_SPAN = 3; |
michael@0 | 129 | // For Editable.removeSpan() call; use with IME_SYNCHRONIZE |
michael@0 | 130 | static final int TYPE_REMOVE_SPAN = 4; |
michael@0 | 131 | // For focus events (in notifyIME); use with IME_ACKNOWLEDGE_FOCUS |
michael@0 | 132 | static final int TYPE_ACKNOWLEDGE_FOCUS = 5; |
michael@0 | 133 | // For switching handler; use with IME_SYNCHRONIZE |
michael@0 | 134 | static final int TYPE_SET_HANDLER = 6; |
michael@0 | 135 | |
michael@0 | 136 | final int mType; |
michael@0 | 137 | int mStart; |
michael@0 | 138 | int mEnd; |
michael@0 | 139 | CharSequence mSequence; |
michael@0 | 140 | Object mSpanObject; |
michael@0 | 141 | int mSpanFlags; |
michael@0 | 142 | boolean mShouldUpdate; |
michael@0 | 143 | Handler mHandler; |
michael@0 | 144 | |
michael@0 | 145 | Action(int type) { |
michael@0 | 146 | mType = type; |
michael@0 | 147 | } |
michael@0 | 148 | |
michael@0 | 149 | static Action newReplaceText(CharSequence text, int start, int end) { |
michael@0 | 150 | if (start < 0 || start > end) { |
michael@0 | 151 | throw new IllegalArgumentException( |
michael@0 | 152 | "invalid replace text offsets: " + start + " to " + end); |
michael@0 | 153 | } |
michael@0 | 154 | final Action action = new Action(TYPE_REPLACE_TEXT); |
michael@0 | 155 | action.mSequence = text; |
michael@0 | 156 | action.mStart = start; |
michael@0 | 157 | action.mEnd = end; |
michael@0 | 158 | return action; |
michael@0 | 159 | } |
michael@0 | 160 | |
michael@0 | 161 | static Action newSetSelection(int start, int end) { |
michael@0 | 162 | // start == -1 when the start offset should remain the same |
michael@0 | 163 | // end == -1 when the end offset should remain the same |
michael@0 | 164 | if (start < -1 || end < -1) { |
michael@0 | 165 | throw new IllegalArgumentException( |
michael@0 | 166 | "invalid selection offsets: " + start + " to " + end); |
michael@0 | 167 | } |
michael@0 | 168 | final Action action = new Action(TYPE_SET_SELECTION); |
michael@0 | 169 | action.mStart = start; |
michael@0 | 170 | action.mEnd = end; |
michael@0 | 171 | return action; |
michael@0 | 172 | } |
michael@0 | 173 | |
michael@0 | 174 | static Action newSetSpan(Object object, int start, int end, int flags) { |
michael@0 | 175 | if (start < 0 || start > end) { |
michael@0 | 176 | throw new IllegalArgumentException( |
michael@0 | 177 | "invalid span offsets: " + start + " to " + end); |
michael@0 | 178 | } |
michael@0 | 179 | final Action action = new Action(TYPE_SET_SPAN); |
michael@0 | 180 | action.mSpanObject = object; |
michael@0 | 181 | action.mStart = start; |
michael@0 | 182 | action.mEnd = end; |
michael@0 | 183 | action.mSpanFlags = flags; |
michael@0 | 184 | return action; |
michael@0 | 185 | } |
michael@0 | 186 | |
michael@0 | 187 | static Action newSetHandler(Handler handler) { |
michael@0 | 188 | final Action action = new Action(TYPE_SET_HANDLER); |
michael@0 | 189 | action.mHandler = handler; |
michael@0 | 190 | return action; |
michael@0 | 191 | } |
michael@0 | 192 | } |
michael@0 | 193 | |
michael@0 | 194 | /* Queue of editing actions sent to Gecko thread that |
michael@0 | 195 | the Gecko thread has not responded to yet */ |
michael@0 | 196 | private final class ActionQueue { |
michael@0 | 197 | private final ConcurrentLinkedQueue<Action> mActions; |
michael@0 | 198 | private final Semaphore mActionsActive; |
michael@0 | 199 | private KeyCharacterMap mKeyMap; |
michael@0 | 200 | |
michael@0 | 201 | ActionQueue() { |
michael@0 | 202 | mActions = new ConcurrentLinkedQueue<Action>(); |
michael@0 | 203 | mActionsActive = new Semaphore(1); |
michael@0 | 204 | } |
michael@0 | 205 | |
michael@0 | 206 | void offer(Action action) { |
michael@0 | 207 | if (DEBUG) { |
michael@0 | 208 | assertOnIcThread(); |
michael@0 | 209 | Log.d(LOGTAG, "offer: Action(" + |
michael@0 | 210 | getConstantName(Action.class, "TYPE_", action.mType) + ")"); |
michael@0 | 211 | } |
michael@0 | 212 | /* Events don't need update because they generate text/selection |
michael@0 | 213 | notifications which will do the updating for us */ |
michael@0 | 214 | if (action.mType != Action.TYPE_EVENT && |
michael@0 | 215 | action.mType != Action.TYPE_ACKNOWLEDGE_FOCUS && |
michael@0 | 216 | action.mType != Action.TYPE_SET_HANDLER) { |
michael@0 | 217 | action.mShouldUpdate = mUpdateGecko; |
michael@0 | 218 | } |
michael@0 | 219 | if (mActions.isEmpty()) { |
michael@0 | 220 | mActionsActive.acquireUninterruptibly(); |
michael@0 | 221 | mActions.offer(action); |
michael@0 | 222 | } else synchronized(this) { |
michael@0 | 223 | // tryAcquire here in case Gecko thread has just released it |
michael@0 | 224 | mActionsActive.tryAcquire(); |
michael@0 | 225 | mActions.offer(action); |
michael@0 | 226 | } |
michael@0 | 227 | switch (action.mType) { |
michael@0 | 228 | case Action.TYPE_EVENT: |
michael@0 | 229 | case Action.TYPE_SET_SELECTION: |
michael@0 | 230 | case Action.TYPE_SET_SPAN: |
michael@0 | 231 | case Action.TYPE_REMOVE_SPAN: |
michael@0 | 232 | case Action.TYPE_SET_HANDLER: |
michael@0 | 233 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( |
michael@0 | 234 | GeckoEvent.ImeAction.IME_SYNCHRONIZE)); |
michael@0 | 235 | break; |
michael@0 | 236 | case Action.TYPE_REPLACE_TEXT: |
michael@0 | 237 | // try key events first |
michael@0 | 238 | sendCharKeyEvents(action); |
michael@0 | 239 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEReplaceEvent( |
michael@0 | 240 | action.mStart, action.mEnd, action.mSequence.toString())); |
michael@0 | 241 | break; |
michael@0 | 242 | case Action.TYPE_ACKNOWLEDGE_FOCUS: |
michael@0 | 243 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( |
michael@0 | 244 | GeckoEvent.ImeAction.IME_ACKNOWLEDGE_FOCUS)); |
michael@0 | 245 | break; |
michael@0 | 246 | } |
michael@0 | 247 | ++mIcUpdateSeqno; |
michael@0 | 248 | } |
michael@0 | 249 | |
michael@0 | 250 | private KeyEvent [] synthesizeKeyEvents(CharSequence cs) { |
michael@0 | 251 | try { |
michael@0 | 252 | if (mKeyMap == null) { |
michael@0 | 253 | mKeyMap = KeyCharacterMap.load( |
michael@0 | 254 | Build.VERSION.SDK_INT < 11 ? KeyCharacterMap.ALPHA : |
michael@0 | 255 | KeyCharacterMap.VIRTUAL_KEYBOARD); |
michael@0 | 256 | } |
michael@0 | 257 | } catch (Exception e) { |
michael@0 | 258 | // KeyCharacterMap.UnavailableExcepton is not found on Gingerbread; |
michael@0 | 259 | // besides, it seems like HC and ICS will throw something other than |
michael@0 | 260 | // KeyCharacterMap.UnavailableExcepton; so use a generic Exception here |
michael@0 | 261 | return null; |
michael@0 | 262 | } |
michael@0 | 263 | KeyEvent [] keyEvents = mKeyMap.getEvents(cs.toString().toCharArray()); |
michael@0 | 264 | if (keyEvents == null || keyEvents.length == 0) { |
michael@0 | 265 | return null; |
michael@0 | 266 | } |
michael@0 | 267 | return keyEvents; |
michael@0 | 268 | } |
michael@0 | 269 | |
michael@0 | 270 | private void sendCharKeyEvents(Action action) { |
michael@0 | 271 | if (action.mSequence.length() == 0 || |
michael@0 | 272 | (action.mSequence instanceof Spannable && |
michael@0 | 273 | ((Spannable)action.mSequence).nextSpanTransition( |
michael@0 | 274 | -1, Integer.MAX_VALUE, null) < Integer.MAX_VALUE)) { |
michael@0 | 275 | // Spans are not preserved when we use key events, |
michael@0 | 276 | // so we need the sequence to not have any spans |
michael@0 | 277 | return; |
michael@0 | 278 | } |
michael@0 | 279 | KeyEvent [] keyEvents = synthesizeKeyEvents(action.mSequence); |
michael@0 | 280 | if (keyEvents == null) { |
michael@0 | 281 | return; |
michael@0 | 282 | } |
michael@0 | 283 | for (KeyEvent event : keyEvents) { |
michael@0 | 284 | if (KeyEvent.isModifierKey(event.getKeyCode())) { |
michael@0 | 285 | continue; |
michael@0 | 286 | } |
michael@0 | 287 | if (event.getAction() == KeyEvent.ACTION_UP && mSuppressKeyUp) { |
michael@0 | 288 | continue; |
michael@0 | 289 | } |
michael@0 | 290 | if (DEBUG) { |
michael@0 | 291 | Log.d(LOGTAG, "sending: " + event); |
michael@0 | 292 | } |
michael@0 | 293 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEKeyEvent(event)); |
michael@0 | 294 | } |
michael@0 | 295 | } |
michael@0 | 296 | |
michael@0 | 297 | void poll() { |
michael@0 | 298 | if (DEBUG) { |
michael@0 | 299 | ThreadUtils.assertOnGeckoThread(); |
michael@0 | 300 | } |
michael@0 | 301 | if (mActions.isEmpty()) { |
michael@0 | 302 | throw new IllegalStateException("empty actions queue"); |
michael@0 | 303 | } |
michael@0 | 304 | mActions.poll(); |
michael@0 | 305 | // Don't bother locking if queue is not empty yet |
michael@0 | 306 | if (mActions.isEmpty()) { |
michael@0 | 307 | synchronized(this) { |
michael@0 | 308 | if (mActions.isEmpty()) { |
michael@0 | 309 | mActionsActive.release(); |
michael@0 | 310 | } |
michael@0 | 311 | } |
michael@0 | 312 | } |
michael@0 | 313 | } |
michael@0 | 314 | |
michael@0 | 315 | Action peek() { |
michael@0 | 316 | if (DEBUG) { |
michael@0 | 317 | ThreadUtils.assertOnGeckoThread(); |
michael@0 | 318 | } |
michael@0 | 319 | if (mActions.isEmpty()) { |
michael@0 | 320 | throw new IllegalStateException("empty actions queue"); |
michael@0 | 321 | } |
michael@0 | 322 | return mActions.peek(); |
michael@0 | 323 | } |
michael@0 | 324 | |
michael@0 | 325 | void syncWithGecko() { |
michael@0 | 326 | if (DEBUG) { |
michael@0 | 327 | assertOnIcThread(); |
michael@0 | 328 | } |
michael@0 | 329 | if (mFocused && !mActions.isEmpty()) { |
michael@0 | 330 | if (DEBUG) { |
michael@0 | 331 | Log.d(LOGTAG, "syncWithGecko blocking on thread " + |
michael@0 | 332 | Thread.currentThread().getName()); |
michael@0 | 333 | } |
michael@0 | 334 | mActionsActive.acquireUninterruptibly(); |
michael@0 | 335 | mActionsActive.release(); |
michael@0 | 336 | } else if (DEBUG && !mFocused) { |
michael@0 | 337 | Log.d(LOGTAG, "skipped syncWithGecko (no focus)"); |
michael@0 | 338 | } |
michael@0 | 339 | } |
michael@0 | 340 | |
michael@0 | 341 | boolean isEmpty() { |
michael@0 | 342 | return mActions.isEmpty(); |
michael@0 | 343 | } |
michael@0 | 344 | } |
michael@0 | 345 | |
michael@0 | 346 | GeckoEditable() { |
michael@0 | 347 | mActionQueue = new ActionQueue(); |
michael@0 | 348 | mSavedSelectionStart = -1; |
michael@0 | 349 | mUpdateGecko = true; |
michael@0 | 350 | |
michael@0 | 351 | mText = new SpannableStringBuilder(); |
michael@0 | 352 | mChangedText = new SpannableStringBuilder(); |
michael@0 | 353 | |
michael@0 | 354 | final Class<?>[] PROXY_INTERFACES = { Editable.class }; |
michael@0 | 355 | mProxy = (Editable)Proxy.newProxyInstance( |
michael@0 | 356 | Editable.class.getClassLoader(), |
michael@0 | 357 | PROXY_INTERFACES, this); |
michael@0 | 358 | |
michael@0 | 359 | LayerView v = GeckoAppShell.getLayerView(); |
michael@0 | 360 | mListener = GeckoInputConnection.create(v, this); |
michael@0 | 361 | |
michael@0 | 362 | mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler(); |
michael@0 | 363 | } |
michael@0 | 364 | |
michael@0 | 365 | private boolean onIcThread() { |
michael@0 | 366 | return mIcRunHandler.getLooper() == Looper.myLooper(); |
michael@0 | 367 | } |
michael@0 | 368 | |
michael@0 | 369 | private void assertOnIcThread() { |
michael@0 | 370 | ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW); |
michael@0 | 371 | } |
michael@0 | 372 | |
michael@0 | 373 | private void geckoPostToIc(Runnable runnable) { |
michael@0 | 374 | mIcPostHandler.post(runnable); |
michael@0 | 375 | } |
michael@0 | 376 | |
michael@0 | 377 | private void geckoUpdateGecko(final boolean force) { |
michael@0 | 378 | /* We do not increment the seqno here, but only check it, because geckoUpdateGecko is a |
michael@0 | 379 | request for update. If we incremented the seqno here, geckoUpdateGecko would have |
michael@0 | 380 | prevented other updates from occurring */ |
michael@0 | 381 | final int seqnoWhenPosted = mGeckoUpdateSeqno; |
michael@0 | 382 | |
michael@0 | 383 | geckoPostToIc(new Runnable() { |
michael@0 | 384 | @Override |
michael@0 | 385 | public void run() { |
michael@0 | 386 | mActionQueue.syncWithGecko(); |
michael@0 | 387 | if (seqnoWhenPosted == mGeckoUpdateSeqno) { |
michael@0 | 388 | icUpdateGecko(force); |
michael@0 | 389 | } |
michael@0 | 390 | } |
michael@0 | 391 | }); |
michael@0 | 392 | } |
michael@0 | 393 | |
michael@0 | 394 | private Object getField(Object obj, String field, Object def) { |
michael@0 | 395 | try { |
michael@0 | 396 | return obj.getClass().getField(field).get(obj); |
michael@0 | 397 | } catch (Exception e) { |
michael@0 | 398 | return def; |
michael@0 | 399 | } |
michael@0 | 400 | } |
michael@0 | 401 | |
michael@0 | 402 | private void icUpdateGecko(boolean force) { |
michael@0 | 403 | |
michael@0 | 404 | // Skip if receiving a repeated request, or |
michael@0 | 405 | // if suppressing compositions during text selection. |
michael@0 | 406 | if ((!force && mIcUpdateSeqno == mLastIcUpdateSeqno) || |
michael@0 | 407 | mSuppressCompositions) { |
michael@0 | 408 | if (DEBUG) { |
michael@0 | 409 | Log.d(LOGTAG, "icUpdateGecko() skipped"); |
michael@0 | 410 | } |
michael@0 | 411 | return; |
michael@0 | 412 | } |
michael@0 | 413 | mLastIcUpdateSeqno = mIcUpdateSeqno; |
michael@0 | 414 | mActionQueue.syncWithGecko(); |
michael@0 | 415 | |
michael@0 | 416 | if (DEBUG) { |
michael@0 | 417 | Log.d(LOGTAG, "icUpdateGecko()"); |
michael@0 | 418 | } |
michael@0 | 419 | |
michael@0 | 420 | final int selStart = mText.getSpanStart(Selection.SELECTION_START); |
michael@0 | 421 | final int selEnd = mText.getSpanEnd(Selection.SELECTION_END); |
michael@0 | 422 | int composingStart = mText.length(); |
michael@0 | 423 | int composingEnd = 0; |
michael@0 | 424 | Object[] spans = mText.getSpans(0, composingStart, Object.class); |
michael@0 | 425 | |
michael@0 | 426 | for (Object span : spans) { |
michael@0 | 427 | if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { |
michael@0 | 428 | composingStart = Math.min(composingStart, mText.getSpanStart(span)); |
michael@0 | 429 | composingEnd = Math.max(composingEnd, mText.getSpanEnd(span)); |
michael@0 | 430 | } |
michael@0 | 431 | } |
michael@0 | 432 | if (DEBUG) { |
michael@0 | 433 | Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd); |
michael@0 | 434 | Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd); |
michael@0 | 435 | } |
michael@0 | 436 | if (composingStart >= composingEnd) { |
michael@0 | 437 | if (selStart >= 0 && selEnd >= 0) { |
michael@0 | 438 | GeckoAppShell.sendEventToGecko( |
michael@0 | 439 | GeckoEvent.createIMESelectEvent(selStart, selEnd)); |
michael@0 | 440 | } else { |
michael@0 | 441 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMEEvent( |
michael@0 | 442 | GeckoEvent.ImeAction.IME_REMOVE_COMPOSITION)); |
michael@0 | 443 | } |
michael@0 | 444 | return; |
michael@0 | 445 | } |
michael@0 | 446 | |
michael@0 | 447 | if (selEnd >= composingStart && selEnd <= composingEnd) { |
michael@0 | 448 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMERangeEvent( |
michael@0 | 449 | selEnd - composingStart, selEnd - composingStart, |
michael@0 | 450 | GeckoEvent.IME_RANGE_CARETPOSITION, 0, 0, false, 0, 0, 0)); |
michael@0 | 451 | } |
michael@0 | 452 | int rangeStart = composingStart; |
michael@0 | 453 | TextPaint tp = new TextPaint(); |
michael@0 | 454 | TextPaint emptyTp = new TextPaint(); |
michael@0 | 455 | // set initial foreground color to 0, because we check for tp.getColor() == 0 |
michael@0 | 456 | // below to decide whether to pass a foreground color to Gecko |
michael@0 | 457 | emptyTp.setColor(0); |
michael@0 | 458 | do { |
michael@0 | 459 | int rangeType, rangeStyles = 0, rangeLineStyle = GeckoEvent.IME_RANGE_LINE_NONE; |
michael@0 | 460 | boolean rangeBoldLine = false; |
michael@0 | 461 | int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0; |
michael@0 | 462 | int rangeEnd = mText.nextSpanTransition(rangeStart, composingEnd, Object.class); |
michael@0 | 463 | |
michael@0 | 464 | if (selStart > rangeStart && selStart < rangeEnd) { |
michael@0 | 465 | rangeEnd = selStart; |
michael@0 | 466 | } else if (selEnd > rangeStart && selEnd < rangeEnd) { |
michael@0 | 467 | rangeEnd = selEnd; |
michael@0 | 468 | } |
michael@0 | 469 | CharacterStyle[] styleSpans = |
michael@0 | 470 | mText.getSpans(rangeStart, rangeEnd, CharacterStyle.class); |
michael@0 | 471 | |
michael@0 | 472 | if (DEBUG) { |
michael@0 | 473 | Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " + |
michael@0 | 474 | rangeStart + "-" + rangeEnd); |
michael@0 | 475 | } |
michael@0 | 476 | |
michael@0 | 477 | if (styleSpans.length == 0) { |
michael@0 | 478 | rangeType = (selStart == rangeStart && selEnd == rangeEnd) |
michael@0 | 479 | ? GeckoEvent.IME_RANGE_SELECTEDRAWTEXT |
michael@0 | 480 | : GeckoEvent.IME_RANGE_RAWINPUT; |
michael@0 | 481 | } else { |
michael@0 | 482 | rangeType = (selStart == rangeStart && selEnd == rangeEnd) |
michael@0 | 483 | ? GeckoEvent.IME_RANGE_SELECTEDCONVERTEDTEXT |
michael@0 | 484 | : GeckoEvent.IME_RANGE_CONVERTEDTEXT; |
michael@0 | 485 | tp.set(emptyTp); |
michael@0 | 486 | for (CharacterStyle span : styleSpans) { |
michael@0 | 487 | span.updateDrawState(tp); |
michael@0 | 488 | } |
michael@0 | 489 | int tpUnderlineColor = 0; |
michael@0 | 490 | float tpUnderlineThickness = 0.0f; |
michael@0 | 491 | // These TextPaint fields only exist on Android ICS+ and are not in the SDK |
michael@0 | 492 | if (Build.VERSION.SDK_INT >= 14) { |
michael@0 | 493 | tpUnderlineColor = (Integer)getField(tp, "underlineColor", 0); |
michael@0 | 494 | tpUnderlineThickness = (Float)getField(tp, "underlineThickness", 0.0f); |
michael@0 | 495 | } |
michael@0 | 496 | if (tpUnderlineColor != 0) { |
michael@0 | 497 | rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE | GeckoEvent.IME_RANGE_LINECOLOR; |
michael@0 | 498 | rangeLineColor = tpUnderlineColor; |
michael@0 | 499 | // Approximately translate underline thickness to what Gecko understands |
michael@0 | 500 | if (tpUnderlineThickness <= 0.5f) { |
michael@0 | 501 | rangeLineStyle = GeckoEvent.IME_RANGE_LINE_DOTTED; |
michael@0 | 502 | } else { |
michael@0 | 503 | rangeLineStyle = GeckoEvent.IME_RANGE_LINE_SOLID; |
michael@0 | 504 | if (tpUnderlineThickness >= 2.0f) { |
michael@0 | 505 | rangeBoldLine = true; |
michael@0 | 506 | } |
michael@0 | 507 | } |
michael@0 | 508 | } else if (tp.isUnderlineText()) { |
michael@0 | 509 | rangeStyles |= GeckoEvent.IME_RANGE_UNDERLINE; |
michael@0 | 510 | rangeLineStyle = GeckoEvent.IME_RANGE_LINE_SOLID; |
michael@0 | 511 | } |
michael@0 | 512 | if (tp.getColor() != 0) { |
michael@0 | 513 | rangeStyles |= GeckoEvent.IME_RANGE_FORECOLOR; |
michael@0 | 514 | rangeForeColor = tp.getColor(); |
michael@0 | 515 | } |
michael@0 | 516 | if (tp.bgColor != 0) { |
michael@0 | 517 | rangeStyles |= GeckoEvent.IME_RANGE_BACKCOLOR; |
michael@0 | 518 | rangeBackColor = tp.bgColor; |
michael@0 | 519 | } |
michael@0 | 520 | } |
michael@0 | 521 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMERangeEvent( |
michael@0 | 522 | rangeStart - composingStart, rangeEnd - composingStart, |
michael@0 | 523 | rangeType, rangeStyles, rangeLineStyle, rangeBoldLine, |
michael@0 | 524 | rangeForeColor, rangeBackColor, rangeLineColor)); |
michael@0 | 525 | rangeStart = rangeEnd; |
michael@0 | 526 | |
michael@0 | 527 | if (DEBUG) { |
michael@0 | 528 | Log.d(LOGTAG, " added " + rangeType + |
michael@0 | 529 | " : " + Integer.toHexString(rangeStyles) + |
michael@0 | 530 | " : " + Integer.toHexString(rangeForeColor) + |
michael@0 | 531 | " : " + Integer.toHexString(rangeBackColor)); |
michael@0 | 532 | } |
michael@0 | 533 | } while (rangeStart < composingEnd); |
michael@0 | 534 | |
michael@0 | 535 | GeckoAppShell.sendEventToGecko(GeckoEvent.createIMECompositionEvent( |
michael@0 | 536 | composingStart, composingEnd)); |
michael@0 | 537 | } |
michael@0 | 538 | |
michael@0 | 539 | // GeckoEditableClient interface |
michael@0 | 540 | |
michael@0 | 541 | @Override |
michael@0 | 542 | public void sendEvent(final GeckoEvent event) { |
michael@0 | 543 | if (DEBUG) { |
michael@0 | 544 | assertOnIcThread(); |
michael@0 | 545 | Log.d(LOGTAG, "sendEvent(" + event + ")"); |
michael@0 | 546 | } |
michael@0 | 547 | /* |
michael@0 | 548 | We are actually sending two events to Gecko here, |
michael@0 | 549 | 1. Event from the event parameter (key event, etc.) |
michael@0 | 550 | 2. Sync event from the mActionQueue.offer call |
michael@0 | 551 | The first event is a normal GeckoEvent that does not reply back to us, |
michael@0 | 552 | the second sync event will have a reply, during which we see that there is a pending |
michael@0 | 553 | event-type action, and update the selection/composition/etc. accordingly. |
michael@0 | 554 | */ |
michael@0 | 555 | GeckoAppShell.sendEventToGecko(event); |
michael@0 | 556 | mActionQueue.offer(new Action(Action.TYPE_EVENT)); |
michael@0 | 557 | } |
michael@0 | 558 | |
michael@0 | 559 | @Override |
michael@0 | 560 | public Editable getEditable() { |
michael@0 | 561 | if (!onIcThread()) { |
michael@0 | 562 | // Android may be holding an old InputConnection; ignore |
michael@0 | 563 | if (DEBUG) { |
michael@0 | 564 | Log.i(LOGTAG, "getEditable() called on non-IC thread"); |
michael@0 | 565 | } |
michael@0 | 566 | return null; |
michael@0 | 567 | } |
michael@0 | 568 | return mProxy; |
michael@0 | 569 | } |
michael@0 | 570 | |
michael@0 | 571 | @Override |
michael@0 | 572 | public void setUpdateGecko(boolean update) { |
michael@0 | 573 | if (!onIcThread()) { |
michael@0 | 574 | // Android may be holding an old InputConnection; ignore |
michael@0 | 575 | if (DEBUG) { |
michael@0 | 576 | Log.i(LOGTAG, "setUpdateGecko() called on non-IC thread"); |
michael@0 | 577 | } |
michael@0 | 578 | return; |
michael@0 | 579 | } |
michael@0 | 580 | if (update) { |
michael@0 | 581 | icUpdateGecko(false); |
michael@0 | 582 | } |
michael@0 | 583 | mUpdateGecko = update; |
michael@0 | 584 | } |
michael@0 | 585 | |
michael@0 | 586 | @Override |
michael@0 | 587 | public void setSuppressKeyUp(boolean suppress) { |
michael@0 | 588 | if (DEBUG) { |
michael@0 | 589 | // only used by key event handler |
michael@0 | 590 | ThreadUtils.assertOnUiThread(); |
michael@0 | 591 | } |
michael@0 | 592 | // Suppress key up event generated as a result of |
michael@0 | 593 | // translating characters to key events |
michael@0 | 594 | mSuppressKeyUp = suppress; |
michael@0 | 595 | } |
michael@0 | 596 | |
michael@0 | 597 | @Override |
michael@0 | 598 | public Handler getInputConnectionHandler() { |
michael@0 | 599 | // Can be called from either UI thread or IC thread; |
michael@0 | 600 | // care must be taken to avoid race conditions |
michael@0 | 601 | return mIcRunHandler; |
michael@0 | 602 | } |
michael@0 | 603 | |
michael@0 | 604 | @Override |
michael@0 | 605 | public boolean setInputConnectionHandler(Handler handler) { |
michael@0 | 606 | if (handler == mIcPostHandler) { |
michael@0 | 607 | return true; |
michael@0 | 608 | } |
michael@0 | 609 | if (!mFocused) { |
michael@0 | 610 | return false; |
michael@0 | 611 | } |
michael@0 | 612 | if (DEBUG) { |
michael@0 | 613 | assertOnIcThread(); |
michael@0 | 614 | } |
michael@0 | 615 | // There are three threads at this point: Gecko thread, old IC thread, and new IC |
michael@0 | 616 | // thread, and we want to safely switch from old IC thread to new IC thread. |
michael@0 | 617 | // We first send a TYPE_SET_HANDLER action to the Gecko thread; this ensures that |
michael@0 | 618 | // the Gecko thread is stopped at a known point. At the same time, the old IC |
michael@0 | 619 | // thread blocks on the action; this ensures that the old IC thread is stopped at |
michael@0 | 620 | // a known point. Finally, inside the Gecko thread, we post a Runnable to the old |
michael@0 | 621 | // IC thread; this Runnable switches from old IC thread to new IC thread. We |
michael@0 | 622 | // switch IC thread on the old IC thread to ensure any pending Runnables on the |
michael@0 | 623 | // old IC thread are processed before we switch over. Inside the Gecko thread, we |
michael@0 | 624 | // also post a Runnable to the new IC thread; this Runnable blocks until the |
michael@0 | 625 | // switch is complete; this ensures that the new IC thread won't accept |
michael@0 | 626 | // InputConnection calls until after the switch. |
michael@0 | 627 | mActionQueue.offer(Action.newSetHandler(handler)); |
michael@0 | 628 | mActionQueue.syncWithGecko(); |
michael@0 | 629 | return true; |
michael@0 | 630 | } |
michael@0 | 631 | |
michael@0 | 632 | private void geckoSetIcHandler(final Handler newHandler) { |
michael@0 | 633 | geckoPostToIc(new Runnable() { // posting to old IC thread |
michael@0 | 634 | @Override |
michael@0 | 635 | public void run() { |
michael@0 | 636 | synchronized (newHandler) { |
michael@0 | 637 | mIcRunHandler = newHandler; |
michael@0 | 638 | newHandler.notify(); |
michael@0 | 639 | } |
michael@0 | 640 | } |
michael@0 | 641 | }); |
michael@0 | 642 | |
michael@0 | 643 | // At this point, all future Runnables should be posted to the new IC thread, but |
michael@0 | 644 | // we don't switch mIcRunHandler yet because there may be pending Runnables on the |
michael@0 | 645 | // old IC thread still waiting to run. |
michael@0 | 646 | mIcPostHandler = newHandler; |
michael@0 | 647 | |
michael@0 | 648 | geckoPostToIc(new Runnable() { // posting to new IC thread |
michael@0 | 649 | @Override |
michael@0 | 650 | public void run() { |
michael@0 | 651 | synchronized (newHandler) { |
michael@0 | 652 | while (mIcRunHandler != newHandler) { |
michael@0 | 653 | try { |
michael@0 | 654 | newHandler.wait(); |
michael@0 | 655 | } catch (InterruptedException e) { |
michael@0 | 656 | } |
michael@0 | 657 | } |
michael@0 | 658 | } |
michael@0 | 659 | } |
michael@0 | 660 | }); |
michael@0 | 661 | } |
michael@0 | 662 | |
michael@0 | 663 | // GeckoEditableListener interface |
michael@0 | 664 | |
michael@0 | 665 | private void geckoActionReply() { |
michael@0 | 666 | if (DEBUG) { |
michael@0 | 667 | // GeckoEditableListener methods should all be called from the Gecko thread |
michael@0 | 668 | ThreadUtils.assertOnGeckoThread(); |
michael@0 | 669 | } |
michael@0 | 670 | final Action action = mActionQueue.peek(); |
michael@0 | 671 | |
michael@0 | 672 | if (DEBUG) { |
michael@0 | 673 | Log.d(LOGTAG, "reply: Action(" + |
michael@0 | 674 | getConstantName(Action.class, "TYPE_", action.mType) + ")"); |
michael@0 | 675 | } |
michael@0 | 676 | switch (action.mType) { |
michael@0 | 677 | case Action.TYPE_SET_SELECTION: |
michael@0 | 678 | final int len = mText.length(); |
michael@0 | 679 | final int curStart = Selection.getSelectionStart(mText); |
michael@0 | 680 | final int curEnd = Selection.getSelectionEnd(mText); |
michael@0 | 681 | // start == -1 when the start offset should remain the same |
michael@0 | 682 | // end == -1 when the end offset should remain the same |
michael@0 | 683 | final int selStart = Math.min(action.mStart < 0 ? curStart : action.mStart, len); |
michael@0 | 684 | final int selEnd = Math.min(action.mEnd < 0 ? curEnd : action.mEnd, len); |
michael@0 | 685 | |
michael@0 | 686 | if (selStart < action.mStart || selEnd < action.mEnd) { |
michael@0 | 687 | Log.w(LOGTAG, "IME sync error: selection out of bounds"); |
michael@0 | 688 | } |
michael@0 | 689 | Selection.setSelection(mText, selStart, selEnd); |
michael@0 | 690 | geckoPostToIc(new Runnable() { |
michael@0 | 691 | @Override |
michael@0 | 692 | public void run() { |
michael@0 | 693 | mActionQueue.syncWithGecko(); |
michael@0 | 694 | final int start = Selection.getSelectionStart(mText); |
michael@0 | 695 | final int end = Selection.getSelectionEnd(mText); |
michael@0 | 696 | if (selStart == start && selEnd == end) { |
michael@0 | 697 | // There has not been another new selection in the mean time that |
michael@0 | 698 | // made this notification out-of-date |
michael@0 | 699 | mListener.onSelectionChange(start, end); |
michael@0 | 700 | } |
michael@0 | 701 | } |
michael@0 | 702 | }); |
michael@0 | 703 | break; |
michael@0 | 704 | case Action.TYPE_SET_SPAN: |
michael@0 | 705 | mText.setSpan(action.mSpanObject, action.mStart, action.mEnd, action.mSpanFlags); |
michael@0 | 706 | break; |
michael@0 | 707 | case Action.TYPE_SET_HANDLER: |
michael@0 | 708 | geckoSetIcHandler(action.mHandler); |
michael@0 | 709 | break; |
michael@0 | 710 | } |
michael@0 | 711 | if (action.mShouldUpdate) { |
michael@0 | 712 | geckoUpdateGecko(false); |
michael@0 | 713 | } |
michael@0 | 714 | } |
michael@0 | 715 | |
michael@0 | 716 | @Override |
michael@0 | 717 | public void notifyIME(final int type) { |
michael@0 | 718 | if (DEBUG) { |
michael@0 | 719 | // GeckoEditableListener methods should all be called from the Gecko thread |
michael@0 | 720 | ThreadUtils.assertOnGeckoThread(); |
michael@0 | 721 | // NOTIFY_IME_REPLY_EVENT is logged separately, inside geckoActionReply() |
michael@0 | 722 | if (type != NOTIFY_IME_REPLY_EVENT) { |
michael@0 | 723 | Log.d(LOGTAG, "notifyIME(" + |
michael@0 | 724 | getConstantName(GeckoEditableListener.class, "NOTIFY_IME_", type) + |
michael@0 | 725 | ")"); |
michael@0 | 726 | } |
michael@0 | 727 | } |
michael@0 | 728 | if (type == NOTIFY_IME_REPLY_EVENT) { |
michael@0 | 729 | try { |
michael@0 | 730 | if (mFocused) { |
michael@0 | 731 | // When mFocused is false, the reply is for a stale action, |
michael@0 | 732 | // and we should not do anything |
michael@0 | 733 | geckoActionReply(); |
michael@0 | 734 | } else if (DEBUG) { |
michael@0 | 735 | Log.d(LOGTAG, "discarding stale reply"); |
michael@0 | 736 | } |
michael@0 | 737 | } finally { |
michael@0 | 738 | // Ensure action is always removed from queue |
michael@0 | 739 | // even if stale action results in exception in geckoActionReply |
michael@0 | 740 | mActionQueue.poll(); |
michael@0 | 741 | } |
michael@0 | 742 | return; |
michael@0 | 743 | } |
michael@0 | 744 | geckoPostToIc(new Runnable() { |
michael@0 | 745 | @Override |
michael@0 | 746 | public void run() { |
michael@0 | 747 | if (type == NOTIFY_IME_OF_BLUR) { |
michael@0 | 748 | mFocused = false; |
michael@0 | 749 | } else if (type == NOTIFY_IME_OF_FOCUS) { |
michael@0 | 750 | mFocused = true; |
michael@0 | 751 | // Unmask events on the Gecko side |
michael@0 | 752 | mActionQueue.offer(new Action(Action.TYPE_ACKNOWLEDGE_FOCUS)); |
michael@0 | 753 | } |
michael@0 | 754 | // Make sure there are no other things going on. If we sent |
michael@0 | 755 | // GeckoEvent.IME_ACKNOWLEDGE_FOCUS, this line also makes us |
michael@0 | 756 | // wait for Gecko to update us on the newly focused content |
michael@0 | 757 | mActionQueue.syncWithGecko(); |
michael@0 | 758 | mListener.notifyIME(type); |
michael@0 | 759 | } |
michael@0 | 760 | }); |
michael@0 | 761 | |
michael@0 | 762 | // Register/unregister Gecko-side text selection listeners |
michael@0 | 763 | // and update the mGeckoFocused flag. |
michael@0 | 764 | if (type == NOTIFY_IME_OF_BLUR && mGeckoFocused) { |
michael@0 | 765 | // Check for focus here because Gecko may send us a blur before a focus in some |
michael@0 | 766 | // cases, and we don't want to unregister an event that was not registered. |
michael@0 | 767 | mGeckoFocused = false; |
michael@0 | 768 | mSuppressCompositions = false; |
michael@0 | 769 | GeckoAppShell.getEventDispatcher(). |
michael@0 | 770 | unregisterEventListener("TextSelection:DraggingHandle", this); |
michael@0 | 771 | } else if (type == NOTIFY_IME_OF_FOCUS) { |
michael@0 | 772 | mGeckoFocused = true; |
michael@0 | 773 | mSuppressCompositions = false; |
michael@0 | 774 | GeckoAppShell.getEventDispatcher(). |
michael@0 | 775 | registerEventListener("TextSelection:DraggingHandle", this); |
michael@0 | 776 | } |
michael@0 | 777 | } |
michael@0 | 778 | |
michael@0 | 779 | @Override |
michael@0 | 780 | public void notifyIMEContext(final int state, final String typeHint, |
michael@0 | 781 | final String modeHint, final String actionHint) { |
michael@0 | 782 | // Because we want to be able to bind GeckoEditable to the newest LayerView instance, |
michael@0 | 783 | // this can be called from the Java IC thread in addition to the Gecko thread. |
michael@0 | 784 | if (DEBUG) { |
michael@0 | 785 | Log.d(LOGTAG, "notifyIMEContext(" + |
michael@0 | 786 | getConstantName(GeckoEditableListener.class, "IME_STATE_", state) + |
michael@0 | 787 | ", \"" + typeHint + "\", \"" + modeHint + "\", \"" + actionHint + "\")"); |
michael@0 | 788 | } |
michael@0 | 789 | geckoPostToIc(new Runnable() { |
michael@0 | 790 | @Override |
michael@0 | 791 | public void run() { |
michael@0 | 792 | // Make sure there are no other things going on |
michael@0 | 793 | mActionQueue.syncWithGecko(); |
michael@0 | 794 | // Set InputConnectionHandler in notifyIMEContext because |
michael@0 | 795 | // GeckoInputConnection.notifyIMEContext calls restartInput() which will invoke |
michael@0 | 796 | // InputConnectionHandler.onCreateInputConnection |
michael@0 | 797 | LayerView v = GeckoAppShell.getLayerView(); |
michael@0 | 798 | if (v != null) { |
michael@0 | 799 | mListener = GeckoInputConnection.create(v, GeckoEditable.this); |
michael@0 | 800 | v.setInputConnectionHandler((InputConnectionHandler)mListener); |
michael@0 | 801 | mListener.notifyIMEContext(state, typeHint, modeHint, actionHint); |
michael@0 | 802 | } |
michael@0 | 803 | } |
michael@0 | 804 | }); |
michael@0 | 805 | } |
michael@0 | 806 | |
michael@0 | 807 | @Override |
michael@0 | 808 | public void onSelectionChange(final int start, final int end) { |
michael@0 | 809 | if (DEBUG) { |
michael@0 | 810 | // GeckoEditableListener methods should all be called from the Gecko thread |
michael@0 | 811 | ThreadUtils.assertOnGeckoThread(); |
michael@0 | 812 | Log.d(LOGTAG, "onSelectionChange(" + start + ", " + end + ")"); |
michael@0 | 813 | } |
michael@0 | 814 | if (start < 0 || start > mText.length() || end < 0 || end > mText.length()) { |
michael@0 | 815 | throw new IllegalArgumentException("invalid selection notification range: " + |
michael@0 | 816 | start + " to " + end + ", length: " + mText.length()); |
michael@0 | 817 | } |
michael@0 | 818 | final int seqnoWhenPosted = ++mGeckoUpdateSeqno; |
michael@0 | 819 | |
michael@0 | 820 | /* An event (keypress, etc.) has potentially changed the selection, |
michael@0 | 821 | synchronize the selection here. There is not a race with the IC thread |
michael@0 | 822 | because the IC thread should be blocked on the event action */ |
michael@0 | 823 | if (!mActionQueue.isEmpty() && |
michael@0 | 824 | mActionQueue.peek().mType == Action.TYPE_EVENT) { |
michael@0 | 825 | Selection.setSelection(mText, start, end); |
michael@0 | 826 | return; |
michael@0 | 827 | } |
michael@0 | 828 | |
michael@0 | 829 | geckoPostToIc(new Runnable() { |
michael@0 | 830 | @Override |
michael@0 | 831 | public void run() { |
michael@0 | 832 | mActionQueue.syncWithGecko(); |
michael@0 | 833 | /* check to see there has not been another action that potentially changed the |
michael@0 | 834 | selection. If so, we can skip this update because we know there is another |
michael@0 | 835 | update right after this one that will replace the effect of this update */ |
michael@0 | 836 | if (mGeckoUpdateSeqno == seqnoWhenPosted) { |
michael@0 | 837 | /* In this case, Gecko's selection has changed and it's notifying us to change |
michael@0 | 838 | Java's selection. In the normal case, whenever Java's selection changes, |
michael@0 | 839 | we go back and set Gecko's selection as well. However, in this case, |
michael@0 | 840 | since Gecko's selection is already up-to-date, we skip this step. */ |
michael@0 | 841 | boolean oldUpdateGecko = mUpdateGecko; |
michael@0 | 842 | mUpdateGecko = false; |
michael@0 | 843 | Selection.setSelection(mProxy, start, end); |
michael@0 | 844 | mUpdateGecko = oldUpdateGecko; |
michael@0 | 845 | } |
michael@0 | 846 | } |
michael@0 | 847 | }); |
michael@0 | 848 | } |
michael@0 | 849 | |
michael@0 | 850 | private void geckoReplaceText(int start, int oldEnd, CharSequence newText) { |
michael@0 | 851 | // Don't use replace() because Gingerbread has a bug where if the replaced text |
michael@0 | 852 | // has the same spans as the original text, the spans will end up being deleted |
michael@0 | 853 | mText.delete(start, oldEnd); |
michael@0 | 854 | mText.insert(start, newText); |
michael@0 | 855 | } |
michael@0 | 856 | |
michael@0 | 857 | @Override |
michael@0 | 858 | public void onTextChange(final String text, final int start, |
michael@0 | 859 | final int unboundedOldEnd, final int unboundedNewEnd) { |
michael@0 | 860 | if (DEBUG) { |
michael@0 | 861 | // GeckoEditableListener methods should all be called from the Gecko thread |
michael@0 | 862 | ThreadUtils.assertOnGeckoThread(); |
michael@0 | 863 | StringBuilder sb = new StringBuilder("onTextChange("); |
michael@0 | 864 | debugAppend(sb, text); |
michael@0 | 865 | sb.append(", ").append(start).append(", ") |
michael@0 | 866 | .append(unboundedOldEnd).append(", ") |
michael@0 | 867 | .append(unboundedNewEnd).append(")"); |
michael@0 | 868 | Log.d(LOGTAG, sb.toString()); |
michael@0 | 869 | } |
michael@0 | 870 | if (start < 0 || start > unboundedOldEnd) { |
michael@0 | 871 | throw new IllegalArgumentException("invalid text notification range: " + |
michael@0 | 872 | start + " to " + unboundedOldEnd); |
michael@0 | 873 | } |
michael@0 | 874 | /* For the "end" parameters, Gecko can pass in a large |
michael@0 | 875 | number to denote "end of the text". Fix that here */ |
michael@0 | 876 | final int oldEnd = unboundedOldEnd > mText.length() ? mText.length() : unboundedOldEnd; |
michael@0 | 877 | // new end should always match text |
michael@0 | 878 | if (start != 0 && unboundedNewEnd != (start + text.length())) { |
michael@0 | 879 | throw new IllegalArgumentException("newEnd does not match text: " + |
michael@0 | 880 | unboundedNewEnd + " vs " + (start + text.length())); |
michael@0 | 881 | } |
michael@0 | 882 | final int newEnd = start + text.length(); |
michael@0 | 883 | |
michael@0 | 884 | /* Text changes affect the selection as well, and we may not receive another selection |
michael@0 | 885 | update as a result of selection notification masking on the Gecko side; therefore, |
michael@0 | 886 | in order to prevent previous stale selection notifications from occurring, we need |
michael@0 | 887 | to increment the seqno here as well */ |
michael@0 | 888 | ++mGeckoUpdateSeqno; |
michael@0 | 889 | |
michael@0 | 890 | mChangedText.clearSpans(); |
michael@0 | 891 | mChangedText.replace(0, mChangedText.length(), text); |
michael@0 | 892 | // Preserve as many spans as possible |
michael@0 | 893 | TextUtils.copySpansFrom(mText, start, Math.min(oldEnd, newEnd), |
michael@0 | 894 | Object.class, mChangedText, 0); |
michael@0 | 895 | |
michael@0 | 896 | if (!mActionQueue.isEmpty()) { |
michael@0 | 897 | final Action action = mActionQueue.peek(); |
michael@0 | 898 | if (action.mType == Action.TYPE_REPLACE_TEXT && |
michael@0 | 899 | start <= action.mStart && |
michael@0 | 900 | action.mStart + action.mSequence.length() <= newEnd) { |
michael@0 | 901 | |
michael@0 | 902 | // actionNewEnd is the new end of the original replacement action |
michael@0 | 903 | final int actionNewEnd = action.mStart + action.mSequence.length(); |
michael@0 | 904 | int selStart = Selection.getSelectionStart(mText); |
michael@0 | 905 | int selEnd = Selection.getSelectionEnd(mText); |
michael@0 | 906 | |
michael@0 | 907 | // Replace old spans with new spans |
michael@0 | 908 | mChangedText.replace(action.mStart - start, actionNewEnd - start, |
michael@0 | 909 | action.mSequence); |
michael@0 | 910 | geckoReplaceText(start, oldEnd, mChangedText); |
michael@0 | 911 | |
michael@0 | 912 | // delete/insert above might have moved our selection to somewhere else |
michael@0 | 913 | // this happens when the Gecko text change covers a larger range than |
michael@0 | 914 | // the original replacement action. Fix selection here |
michael@0 | 915 | if (selStart >= start && selStart <= oldEnd) { |
michael@0 | 916 | selStart = selStart < action.mStart ? selStart : |
michael@0 | 917 | selStart < action.mEnd ? actionNewEnd : |
michael@0 | 918 | selStart + actionNewEnd - action.mEnd; |
michael@0 | 919 | mText.setSpan(Selection.SELECTION_START, selStart, selStart, |
michael@0 | 920 | Spanned.SPAN_POINT_POINT); |
michael@0 | 921 | } |
michael@0 | 922 | if (selEnd >= start && selEnd <= oldEnd) { |
michael@0 | 923 | selEnd = selEnd < action.mStart ? selEnd : |
michael@0 | 924 | selEnd < action.mEnd ? actionNewEnd : |
michael@0 | 925 | selEnd + actionNewEnd - action.mEnd; |
michael@0 | 926 | mText.setSpan(Selection.SELECTION_END, selEnd, selEnd, |
michael@0 | 927 | Spanned.SPAN_POINT_POINT); |
michael@0 | 928 | } |
michael@0 | 929 | } else { |
michael@0 | 930 | geckoReplaceText(start, oldEnd, mChangedText); |
michael@0 | 931 | } |
michael@0 | 932 | } else { |
michael@0 | 933 | geckoReplaceText(start, oldEnd, mChangedText); |
michael@0 | 934 | } |
michael@0 | 935 | geckoPostToIc(new Runnable() { |
michael@0 | 936 | @Override |
michael@0 | 937 | public void run() { |
michael@0 | 938 | mListener.onTextChange(text, start, oldEnd, newEnd); |
michael@0 | 939 | } |
michael@0 | 940 | }); |
michael@0 | 941 | } |
michael@0 | 942 | |
michael@0 | 943 | // InvocationHandler interface |
michael@0 | 944 | |
michael@0 | 945 | static String getConstantName(Class<?> cls, String prefix, Object value) { |
michael@0 | 946 | for (Field fld : cls.getDeclaredFields()) { |
michael@0 | 947 | try { |
michael@0 | 948 | if (fld.getName().startsWith(prefix) && |
michael@0 | 949 | fld.get(null).equals(value)) { |
michael@0 | 950 | return fld.getName(); |
michael@0 | 951 | } |
michael@0 | 952 | } catch (IllegalAccessException e) { |
michael@0 | 953 | } |
michael@0 | 954 | } |
michael@0 | 955 | return String.valueOf(value); |
michael@0 | 956 | } |
michael@0 | 957 | |
michael@0 | 958 | static StringBuilder debugAppend(StringBuilder sb, Object obj) { |
michael@0 | 959 | if (obj == null) { |
michael@0 | 960 | sb.append("null"); |
michael@0 | 961 | } else if (obj instanceof GeckoEditable) { |
michael@0 | 962 | sb.append("GeckoEditable"); |
michael@0 | 963 | } else if (Proxy.isProxyClass(obj.getClass())) { |
michael@0 | 964 | debugAppend(sb, Proxy.getInvocationHandler(obj)); |
michael@0 | 965 | } else if (obj instanceof CharSequence) { |
michael@0 | 966 | sb.append("\"").append(obj.toString().replace('\n', '\u21b2')).append("\""); |
michael@0 | 967 | } else if (obj.getClass().isArray()) { |
michael@0 | 968 | sb.append(obj.getClass().getComponentType().getSimpleName()).append("[") |
michael@0 | 969 | .append(java.lang.reflect.Array.getLength(obj)).append("]"); |
michael@0 | 970 | } else { |
michael@0 | 971 | sb.append(obj.toString()); |
michael@0 | 972 | } |
michael@0 | 973 | return sb; |
michael@0 | 974 | } |
michael@0 | 975 | |
michael@0 | 976 | @Override |
michael@0 | 977 | public Object invoke(Object proxy, Method method, Object[] args) |
michael@0 | 978 | throws Throwable { |
michael@0 | 979 | Object target; |
michael@0 | 980 | final Class<?> methodInterface = method.getDeclaringClass(); |
michael@0 | 981 | if (DEBUG) { |
michael@0 | 982 | // Editable methods should all be called from the IC thread |
michael@0 | 983 | assertOnIcThread(); |
michael@0 | 984 | } |
michael@0 | 985 | if (methodInterface == Editable.class || |
michael@0 | 986 | methodInterface == Appendable.class || |
michael@0 | 987 | methodInterface == Spannable.class) { |
michael@0 | 988 | // Method alters the Editable; route calls to our implementation |
michael@0 | 989 | target = this; |
michael@0 | 990 | } else { |
michael@0 | 991 | // Method queries the Editable; must sync with Gecko first |
michael@0 | 992 | // then call on the inner Editable itself |
michael@0 | 993 | mActionQueue.syncWithGecko(); |
michael@0 | 994 | target = mText; |
michael@0 | 995 | } |
michael@0 | 996 | Object ret; |
michael@0 | 997 | try { |
michael@0 | 998 | ret = method.invoke(target, args); |
michael@0 | 999 | } catch (InvocationTargetException e) { |
michael@0 | 1000 | // Bug 817386 |
michael@0 | 1001 | // Most likely Gecko has changed the text while GeckoInputConnection is |
michael@0 | 1002 | // trying to access the text. If we pass through the exception here, Fennec |
michael@0 | 1003 | // will crash due to a lack of exception handler. Log the exception and |
michael@0 | 1004 | // return an empty value instead. |
michael@0 | 1005 | if (!(e.getCause() instanceof IndexOutOfBoundsException)) { |
michael@0 | 1006 | // Only handle IndexOutOfBoundsException for now, |
michael@0 | 1007 | // as other exceptions might signal other bugs |
michael@0 | 1008 | throw e; |
michael@0 | 1009 | } |
michael@0 | 1010 | Log.w(LOGTAG, "Exception in GeckoEditable." + method.getName(), e.getCause()); |
michael@0 | 1011 | Class<?> retClass = method.getReturnType(); |
michael@0 | 1012 | if (retClass == Character.TYPE) { |
michael@0 | 1013 | ret = '\0'; |
michael@0 | 1014 | } else if (retClass == Integer.TYPE) { |
michael@0 | 1015 | ret = 0; |
michael@0 | 1016 | } else if (retClass == String.class) { |
michael@0 | 1017 | ret = ""; |
michael@0 | 1018 | } else { |
michael@0 | 1019 | ret = null; |
michael@0 | 1020 | } |
michael@0 | 1021 | } |
michael@0 | 1022 | if (DEBUG) { |
michael@0 | 1023 | StringBuilder log = new StringBuilder(method.getName()); |
michael@0 | 1024 | log.append("("); |
michael@0 | 1025 | for (Object arg : args) { |
michael@0 | 1026 | debugAppend(log, arg).append(", "); |
michael@0 | 1027 | } |
michael@0 | 1028 | if (args.length > 0) { |
michael@0 | 1029 | log.setLength(log.length() - 2); |
michael@0 | 1030 | } |
michael@0 | 1031 | if (method.getReturnType().equals(Void.TYPE)) { |
michael@0 | 1032 | log.append(")"); |
michael@0 | 1033 | } else { |
michael@0 | 1034 | debugAppend(log.append(") = "), ret); |
michael@0 | 1035 | } |
michael@0 | 1036 | Log.d(LOGTAG, log.toString()); |
michael@0 | 1037 | } |
michael@0 | 1038 | return ret; |
michael@0 | 1039 | } |
michael@0 | 1040 | |
michael@0 | 1041 | // Spannable interface |
michael@0 | 1042 | |
michael@0 | 1043 | @Override |
michael@0 | 1044 | public void removeSpan(Object what) { |
michael@0 | 1045 | if (what == Selection.SELECTION_START || |
michael@0 | 1046 | what == Selection.SELECTION_END) { |
michael@0 | 1047 | Log.w(LOGTAG, "selection removed with removeSpan()"); |
michael@0 | 1048 | } |
michael@0 | 1049 | if (mText.getSpanStart(what) >= 0) { // only remove if it's there |
michael@0 | 1050 | // Okay to remove immediately |
michael@0 | 1051 | mText.removeSpan(what); |
michael@0 | 1052 | mActionQueue.offer(new Action(Action.TYPE_REMOVE_SPAN)); |
michael@0 | 1053 | } |
michael@0 | 1054 | } |
michael@0 | 1055 | |
michael@0 | 1056 | @Override |
michael@0 | 1057 | public void setSpan(Object what, int start, int end, int flags) { |
michael@0 | 1058 | if (what == Selection.SELECTION_START) { |
michael@0 | 1059 | if ((flags & Spanned.SPAN_INTERMEDIATE) != 0) { |
michael@0 | 1060 | // We will get the end offset next, just save the start for now |
michael@0 | 1061 | mSavedSelectionStart = start; |
michael@0 | 1062 | } else { |
michael@0 | 1063 | mActionQueue.offer(Action.newSetSelection(start, -1)); |
michael@0 | 1064 | } |
michael@0 | 1065 | } else if (what == Selection.SELECTION_END) { |
michael@0 | 1066 | mActionQueue.offer(Action.newSetSelection(mSavedSelectionStart, end)); |
michael@0 | 1067 | mSavedSelectionStart = -1; |
michael@0 | 1068 | } else { |
michael@0 | 1069 | mActionQueue.offer(Action.newSetSpan(what, start, end, flags)); |
michael@0 | 1070 | } |
michael@0 | 1071 | } |
michael@0 | 1072 | |
michael@0 | 1073 | // Appendable interface |
michael@0 | 1074 | |
michael@0 | 1075 | @Override |
michael@0 | 1076 | public Editable append(CharSequence text) { |
michael@0 | 1077 | return replace(mProxy.length(), mProxy.length(), text, 0, text.length()); |
michael@0 | 1078 | } |
michael@0 | 1079 | |
michael@0 | 1080 | @Override |
michael@0 | 1081 | public Editable append(CharSequence text, int start, int end) { |
michael@0 | 1082 | return replace(mProxy.length(), mProxy.length(), text, start, end); |
michael@0 | 1083 | } |
michael@0 | 1084 | |
michael@0 | 1085 | @Override |
michael@0 | 1086 | public Editable append(char text) { |
michael@0 | 1087 | return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1); |
michael@0 | 1088 | } |
michael@0 | 1089 | |
michael@0 | 1090 | // Editable interface |
michael@0 | 1091 | |
michael@0 | 1092 | @Override |
michael@0 | 1093 | public InputFilter[] getFilters() { |
michael@0 | 1094 | return mFilters; |
michael@0 | 1095 | } |
michael@0 | 1096 | |
michael@0 | 1097 | @Override |
michael@0 | 1098 | public void setFilters(InputFilter[] filters) { |
michael@0 | 1099 | mFilters = filters; |
michael@0 | 1100 | } |
michael@0 | 1101 | |
michael@0 | 1102 | @Override |
michael@0 | 1103 | public void clearSpans() { |
michael@0 | 1104 | /* XXX this clears the selection spans too, |
michael@0 | 1105 | but there is no way to clear the corresponding selection in Gecko */ |
michael@0 | 1106 | Log.w(LOGTAG, "selection cleared with clearSpans()"); |
michael@0 | 1107 | mText.clearSpans(); |
michael@0 | 1108 | } |
michael@0 | 1109 | |
michael@0 | 1110 | @Override |
michael@0 | 1111 | public Editable replace(int st, int en, |
michael@0 | 1112 | CharSequence source, int start, int end) { |
michael@0 | 1113 | |
michael@0 | 1114 | CharSequence text = source; |
michael@0 | 1115 | if (start < 0 || start > end || end > text.length()) { |
michael@0 | 1116 | throw new IllegalArgumentException("invalid replace offsets: " + |
michael@0 | 1117 | start + " to " + end + ", length: " + text.length()); |
michael@0 | 1118 | } |
michael@0 | 1119 | if (start != 0 || end != text.length()) { |
michael@0 | 1120 | text = text.subSequence(start, end); |
michael@0 | 1121 | } |
michael@0 | 1122 | if (mFilters != null) { |
michael@0 | 1123 | // Filter text before sending the request to Gecko |
michael@0 | 1124 | for (int i = 0; i < mFilters.length; ++i) { |
michael@0 | 1125 | final CharSequence cs = mFilters[i].filter( |
michael@0 | 1126 | text, 0, text.length(), mProxy, st, en); |
michael@0 | 1127 | if (cs != null) { |
michael@0 | 1128 | text = cs; |
michael@0 | 1129 | } |
michael@0 | 1130 | } |
michael@0 | 1131 | } |
michael@0 | 1132 | if (text == source) { |
michael@0 | 1133 | // Always create a copy |
michael@0 | 1134 | text = new SpannableString(source); |
michael@0 | 1135 | } |
michael@0 | 1136 | mActionQueue.offer(Action.newReplaceText(text, |
michael@0 | 1137 | Math.min(st, en), Math.max(st, en))); |
michael@0 | 1138 | return mProxy; |
michael@0 | 1139 | } |
michael@0 | 1140 | |
michael@0 | 1141 | @Override |
michael@0 | 1142 | public void clear() { |
michael@0 | 1143 | replace(0, mProxy.length(), "", 0, 0); |
michael@0 | 1144 | } |
michael@0 | 1145 | |
michael@0 | 1146 | @Override |
michael@0 | 1147 | public Editable delete(int st, int en) { |
michael@0 | 1148 | return replace(st, en, "", 0, 0); |
michael@0 | 1149 | } |
michael@0 | 1150 | |
michael@0 | 1151 | @Override |
michael@0 | 1152 | public Editable insert(int where, CharSequence text, |
michael@0 | 1153 | int start, int end) { |
michael@0 | 1154 | return replace(where, where, text, start, end); |
michael@0 | 1155 | } |
michael@0 | 1156 | |
michael@0 | 1157 | @Override |
michael@0 | 1158 | public Editable insert(int where, CharSequence text) { |
michael@0 | 1159 | return replace(where, where, text, 0, text.length()); |
michael@0 | 1160 | } |
michael@0 | 1161 | |
michael@0 | 1162 | @Override |
michael@0 | 1163 | public Editable replace(int st, int en, CharSequence text) { |
michael@0 | 1164 | return replace(st, en, text, 0, text.length()); |
michael@0 | 1165 | } |
michael@0 | 1166 | |
michael@0 | 1167 | /* GetChars interface */ |
michael@0 | 1168 | |
michael@0 | 1169 | @Override |
michael@0 | 1170 | public void getChars(int start, int end, char[] dest, int destoff) { |
michael@0 | 1171 | /* overridden Editable interface methods in GeckoEditable must not be called directly |
michael@0 | 1172 | outside of GeckoEditable. Instead, the call must go through mProxy, which ensures |
michael@0 | 1173 | that Java is properly synchronized with Gecko */ |
michael@0 | 1174 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1175 | } |
michael@0 | 1176 | |
michael@0 | 1177 | /* Spanned interface */ |
michael@0 | 1178 | |
michael@0 | 1179 | @Override |
michael@0 | 1180 | public int getSpanEnd(Object tag) { |
michael@0 | 1181 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1182 | } |
michael@0 | 1183 | |
michael@0 | 1184 | @Override |
michael@0 | 1185 | public int getSpanFlags(Object tag) { |
michael@0 | 1186 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1187 | } |
michael@0 | 1188 | |
michael@0 | 1189 | @Override |
michael@0 | 1190 | public int getSpanStart(Object tag) { |
michael@0 | 1191 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1192 | } |
michael@0 | 1193 | |
michael@0 | 1194 | @Override |
michael@0 | 1195 | public <T> T[] getSpans(int start, int end, Class<T> type) { |
michael@0 | 1196 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1197 | } |
michael@0 | 1198 | |
michael@0 | 1199 | @Override |
michael@0 | 1200 | @SuppressWarnings("rawtypes") // nextSpanTransition uses raw Class in its Android declaration |
michael@0 | 1201 | public int nextSpanTransition(int start, int limit, Class type) { |
michael@0 | 1202 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1203 | } |
michael@0 | 1204 | |
michael@0 | 1205 | /* CharSequence interface */ |
michael@0 | 1206 | |
michael@0 | 1207 | @Override |
michael@0 | 1208 | public char charAt(int index) { |
michael@0 | 1209 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1210 | } |
michael@0 | 1211 | |
michael@0 | 1212 | @Override |
michael@0 | 1213 | public int length() { |
michael@0 | 1214 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1215 | } |
michael@0 | 1216 | |
michael@0 | 1217 | @Override |
michael@0 | 1218 | public CharSequence subSequence(int start, int end) { |
michael@0 | 1219 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1220 | } |
michael@0 | 1221 | |
michael@0 | 1222 | @Override |
michael@0 | 1223 | public String toString() { |
michael@0 | 1224 | throw new UnsupportedOperationException("method must be called through mProxy"); |
michael@0 | 1225 | } |
michael@0 | 1226 | |
michael@0 | 1227 | // GeckoEventListener implementation |
michael@0 | 1228 | |
michael@0 | 1229 | @Override |
michael@0 | 1230 | public void handleMessage(String event, JSONObject message) { |
michael@0 | 1231 | if (!"TextSelection:DraggingHandle".equals(event)) { |
michael@0 | 1232 | return; |
michael@0 | 1233 | } |
michael@0 | 1234 | |
michael@0 | 1235 | mSuppressCompositions = message.optBoolean("dragging", false); |
michael@0 | 1236 | } |
michael@0 | 1237 | } |
michael@0 | 1238 |