|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko; |
|
7 |
|
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; |
|
13 |
|
14 import org.json.JSONObject; |
|
15 |
|
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; |
|
32 |
|
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; |
|
40 |
|
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 } |
|
50 |
|
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; |
|
66 |
|
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 } |
|
73 |
|
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 { |
|
82 |
|
83 private static final boolean DEBUG = false; |
|
84 private static final String LOGTAG = "GeckoEditable"; |
|
85 |
|
86 // Filters to implement Editable's filtering functionality |
|
87 private InputFilter[] mFilters; |
|
88 |
|
89 private final SpannableStringBuilder mText; |
|
90 private final SpannableStringBuilder mChangedText; |
|
91 private final Editable mProxy; |
|
92 private final ActionQueue mActionQueue; |
|
93 |
|
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; |
|
99 |
|
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; |
|
110 |
|
111 /* An action that alters the Editable |
|
112 |
|
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; |
|
135 |
|
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; |
|
144 |
|
145 Action(int type) { |
|
146 mType = type; |
|
147 } |
|
148 |
|
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 } |
|
160 |
|
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 } |
|
173 |
|
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 } |
|
186 |
|
187 static Action newSetHandler(Handler handler) { |
|
188 final Action action = new Action(TYPE_SET_HANDLER); |
|
189 action.mHandler = handler; |
|
190 return action; |
|
191 } |
|
192 } |
|
193 |
|
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; |
|
200 |
|
201 ActionQueue() { |
|
202 mActions = new ConcurrentLinkedQueue<Action>(); |
|
203 mActionsActive = new Semaphore(1); |
|
204 } |
|
205 |
|
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 } |
|
249 |
|
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 } |
|
269 |
|
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 } |
|
296 |
|
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 } |
|
314 |
|
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 } |
|
324 |
|
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 } |
|
340 |
|
341 boolean isEmpty() { |
|
342 return mActions.isEmpty(); |
|
343 } |
|
344 } |
|
345 |
|
346 GeckoEditable() { |
|
347 mActionQueue = new ActionQueue(); |
|
348 mSavedSelectionStart = -1; |
|
349 mUpdateGecko = true; |
|
350 |
|
351 mText = new SpannableStringBuilder(); |
|
352 mChangedText = new SpannableStringBuilder(); |
|
353 |
|
354 final Class<?>[] PROXY_INTERFACES = { Editable.class }; |
|
355 mProxy = (Editable)Proxy.newProxyInstance( |
|
356 Editable.class.getClassLoader(), |
|
357 PROXY_INTERFACES, this); |
|
358 |
|
359 LayerView v = GeckoAppShell.getLayerView(); |
|
360 mListener = GeckoInputConnection.create(v, this); |
|
361 |
|
362 mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler(); |
|
363 } |
|
364 |
|
365 private boolean onIcThread() { |
|
366 return mIcRunHandler.getLooper() == Looper.myLooper(); |
|
367 } |
|
368 |
|
369 private void assertOnIcThread() { |
|
370 ThreadUtils.assertOnThread(mIcRunHandler.getLooper().getThread(), AssertBehavior.THROW); |
|
371 } |
|
372 |
|
373 private void geckoPostToIc(Runnable runnable) { |
|
374 mIcPostHandler.post(runnable); |
|
375 } |
|
376 |
|
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; |
|
382 |
|
383 geckoPostToIc(new Runnable() { |
|
384 @Override |
|
385 public void run() { |
|
386 mActionQueue.syncWithGecko(); |
|
387 if (seqnoWhenPosted == mGeckoUpdateSeqno) { |
|
388 icUpdateGecko(force); |
|
389 } |
|
390 } |
|
391 }); |
|
392 } |
|
393 |
|
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 } |
|
401 |
|
402 private void icUpdateGecko(boolean force) { |
|
403 |
|
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(); |
|
415 |
|
416 if (DEBUG) { |
|
417 Log.d(LOGTAG, "icUpdateGecko()"); |
|
418 } |
|
419 |
|
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); |
|
425 |
|
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 } |
|
446 |
|
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); |
|
463 |
|
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); |
|
471 |
|
472 if (DEBUG) { |
|
473 Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " + |
|
474 rangeStart + "-" + rangeEnd); |
|
475 } |
|
476 |
|
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; |
|
526 |
|
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); |
|
534 |
|
535 GeckoAppShell.sendEventToGecko(GeckoEvent.createIMECompositionEvent( |
|
536 composingStart, composingEnd)); |
|
537 } |
|
538 |
|
539 // GeckoEditableClient interface |
|
540 |
|
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 } |
|
558 |
|
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 } |
|
570 |
|
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 } |
|
585 |
|
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 } |
|
596 |
|
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 } |
|
603 |
|
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 } |
|
631 |
|
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 }); |
|
642 |
|
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; |
|
647 |
|
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 } |
|
662 |
|
663 // GeckoEditableListener interface |
|
664 |
|
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(); |
|
671 |
|
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); |
|
685 |
|
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 } |
|
715 |
|
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 }); |
|
761 |
|
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 } |
|
778 |
|
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 } |
|
806 |
|
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; |
|
819 |
|
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 } |
|
828 |
|
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 } |
|
849 |
|
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 } |
|
856 |
|
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(); |
|
883 |
|
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; |
|
889 |
|
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); |
|
895 |
|
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) { |
|
901 |
|
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); |
|
906 |
|
907 // Replace old spans with new spans |
|
908 mChangedText.replace(action.mStart - start, actionNewEnd - start, |
|
909 action.mSequence); |
|
910 geckoReplaceText(start, oldEnd, mChangedText); |
|
911 |
|
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 } |
|
942 |
|
943 // InvocationHandler interface |
|
944 |
|
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 } |
|
957 |
|
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 } |
|
975 |
|
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 } |
|
1040 |
|
1041 // Spannable interface |
|
1042 |
|
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 } |
|
1055 |
|
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 } |
|
1072 |
|
1073 // Appendable interface |
|
1074 |
|
1075 @Override |
|
1076 public Editable append(CharSequence text) { |
|
1077 return replace(mProxy.length(), mProxy.length(), text, 0, text.length()); |
|
1078 } |
|
1079 |
|
1080 @Override |
|
1081 public Editable append(CharSequence text, int start, int end) { |
|
1082 return replace(mProxy.length(), mProxy.length(), text, start, end); |
|
1083 } |
|
1084 |
|
1085 @Override |
|
1086 public Editable append(char text) { |
|
1087 return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1); |
|
1088 } |
|
1089 |
|
1090 // Editable interface |
|
1091 |
|
1092 @Override |
|
1093 public InputFilter[] getFilters() { |
|
1094 return mFilters; |
|
1095 } |
|
1096 |
|
1097 @Override |
|
1098 public void setFilters(InputFilter[] filters) { |
|
1099 mFilters = filters; |
|
1100 } |
|
1101 |
|
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 } |
|
1109 |
|
1110 @Override |
|
1111 public Editable replace(int st, int en, |
|
1112 CharSequence source, int start, int end) { |
|
1113 |
|
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 } |
|
1140 |
|
1141 @Override |
|
1142 public void clear() { |
|
1143 replace(0, mProxy.length(), "", 0, 0); |
|
1144 } |
|
1145 |
|
1146 @Override |
|
1147 public Editable delete(int st, int en) { |
|
1148 return replace(st, en, "", 0, 0); |
|
1149 } |
|
1150 |
|
1151 @Override |
|
1152 public Editable insert(int where, CharSequence text, |
|
1153 int start, int end) { |
|
1154 return replace(where, where, text, start, end); |
|
1155 } |
|
1156 |
|
1157 @Override |
|
1158 public Editable insert(int where, CharSequence text) { |
|
1159 return replace(where, where, text, 0, text.length()); |
|
1160 } |
|
1161 |
|
1162 @Override |
|
1163 public Editable replace(int st, int en, CharSequence text) { |
|
1164 return replace(st, en, text, 0, text.length()); |
|
1165 } |
|
1166 |
|
1167 /* GetChars interface */ |
|
1168 |
|
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 } |
|
1176 |
|
1177 /* Spanned interface */ |
|
1178 |
|
1179 @Override |
|
1180 public int getSpanEnd(Object tag) { |
|
1181 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1182 } |
|
1183 |
|
1184 @Override |
|
1185 public int getSpanFlags(Object tag) { |
|
1186 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1187 } |
|
1188 |
|
1189 @Override |
|
1190 public int getSpanStart(Object tag) { |
|
1191 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1192 } |
|
1193 |
|
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 } |
|
1198 |
|
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 } |
|
1204 |
|
1205 /* CharSequence interface */ |
|
1206 |
|
1207 @Override |
|
1208 public char charAt(int index) { |
|
1209 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1210 } |
|
1211 |
|
1212 @Override |
|
1213 public int length() { |
|
1214 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1215 } |
|
1216 |
|
1217 @Override |
|
1218 public CharSequence subSequence(int start, int end) { |
|
1219 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1220 } |
|
1221 |
|
1222 @Override |
|
1223 public String toString() { |
|
1224 throw new UnsupportedOperationException("method must be called through mProxy"); |
|
1225 } |
|
1226 |
|
1227 // GeckoEventListener implementation |
|
1228 |
|
1229 @Override |
|
1230 public void handleMessage(String event, JSONObject message) { |
|
1231 if (!"TextSelection:DraggingHandle".equals(event)) { |
|
1232 return; |
|
1233 } |
|
1234 |
|
1235 mSuppressCompositions = message.optBoolean("dragging", false); |
|
1236 } |
|
1237 } |
|
1238 |