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