mobile/android/base/GeckoEditable.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

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

mercurial