mobile/android/base/GeckoEditable.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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;
  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;
  1022         if (DEBUG) {
  1023             StringBuilder log = new StringBuilder(method.getName());
  1024             log.append("(");
  1025             for (Object arg : args) {
  1026                 debugAppend(log, arg).append(", ");
  1028             if (args.length > 0) {
  1029                 log.setLength(log.length() - 2);
  1031             if (method.getReturnType().equals(Void.TYPE)) {
  1032                 log.append(")");
  1033             } else {
  1034                 debugAppend(log.append(") = "), ret);
  1036             Log.d(LOGTAG, log.toString());
  1038         return ret;
  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()");
  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));
  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));
  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));
  1073     // Appendable interface
  1075     @Override
  1076     public Editable append(CharSequence text) {
  1077         return replace(mProxy.length(), mProxy.length(), text, 0, text.length());
  1080     @Override
  1081     public Editable append(CharSequence text, int start, int end) {
  1082         return replace(mProxy.length(), mProxy.length(), text, start, end);
  1085     @Override
  1086     public Editable append(char text) {
  1087         return replace(mProxy.length(), mProxy.length(), String.valueOf(text), 0, 1);
  1090     // Editable interface
  1092     @Override
  1093     public InputFilter[] getFilters() {
  1094         return mFilters;
  1097     @Override
  1098     public void setFilters(InputFilter[] filters) {
  1099         mFilters = filters;
  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();
  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());
  1119         if (start != 0 || end != text.length()) {
  1120             text = text.subSequence(start, end);
  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;
  1132         if (text == source) {
  1133             // Always create a copy
  1134             text = new SpannableString(source);
  1136         mActionQueue.offer(Action.newReplaceText(text,
  1137                 Math.min(st, en), Math.max(st, en)));
  1138         return mProxy;
  1141     @Override
  1142     public void clear() {
  1143         replace(0, mProxy.length(), "", 0, 0);
  1146     @Override
  1147     public Editable delete(int st, int en) {
  1148         return replace(st, en, "", 0, 0);
  1151     @Override
  1152     public Editable insert(int where, CharSequence text,
  1153                                 int start, int end) {
  1154         return replace(where, where, text, start, end);
  1157     @Override
  1158     public Editable insert(int where, CharSequence text) {
  1159         return replace(where, where, text, 0, text.length());
  1162     @Override
  1163     public Editable replace(int st, int en, CharSequence text) {
  1164         return replace(st, en, text, 0, text.length());
  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");
  1177     /* Spanned interface */
  1179     @Override
  1180     public int getSpanEnd(Object tag) {
  1181         throw new UnsupportedOperationException("method must be called through mProxy");
  1184     @Override
  1185     public int getSpanFlags(Object tag) {
  1186         throw new UnsupportedOperationException("method must be called through mProxy");
  1189     @Override
  1190     public int getSpanStart(Object tag) {
  1191         throw new UnsupportedOperationException("method must be called through mProxy");
  1194     @Override
  1195     public <T> T[] getSpans(int start, int end, Class<T> type) {
  1196         throw new UnsupportedOperationException("method must be called through mProxy");
  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");
  1205     /* CharSequence interface */
  1207     @Override
  1208     public char charAt(int index) {
  1209         throw new UnsupportedOperationException("method must be called through mProxy");
  1212     @Override
  1213     public int length() {
  1214         throw new UnsupportedOperationException("method must be called through mProxy");
  1217     @Override
  1218     public CharSequence subSequence(int start, int end) {
  1219         throw new UnsupportedOperationException("method must be called through mProxy");
  1222     @Override
  1223     public String toString() {
  1224         throw new UnsupportedOperationException("method must be called through mProxy");
  1227     // GeckoEventListener implementation
  1229     @Override
  1230     public void handleMessage(String event, JSONObject message) {
  1231         if (!"TextSelection:DraggingHandle".equals(event)) {
  1232             return;
  1235         mSuppressCompositions = message.optBoolean("dragging", false);

mercurial