Bug 839882 - Provide UI-thread-safe Editable for KeyListener; r=cpeterson
authorJim Chen <nchen@mozilla.com>
Wed, 13 Feb 2013 15:52:11 -0500
changeset 121798 6f8fc3c171840e2d50ebe29de9e5bff9e5ecf3be
parent 121797 a0db951f9f08404b4b0b53d1a8ffedff06d8094a
child 121799 9814c03a6e2eb2b0d082c13c621836316daec1ec
push id24307
push useremorley@mozilla.com
push dateThu, 14 Feb 2013 10:47:46 +0000
treeherdermozilla-central@aceeea086ccb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscpeterson
bugs839882
milestone21.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 839882 - Provide UI-thread-safe Editable for KeyListener; r=cpeterson
mobile/android/base/GeckoInputConnection.java
--- a/mobile/android/base/GeckoInputConnection.java
+++ b/mobile/android/base/GeckoInputConnection.java
@@ -27,28 +27,153 @@ import android.view.inputmethod.EditorIn
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.util.concurrent.SynchronousQueue;
 
 class GeckoInputConnection
     extends BaseInputConnection
     implements InputConnectionHandler, GeckoEditableListener {
 
     private static final boolean DEBUG = false;
     protected static final String LOGTAG = "GeckoInputConnection";
 
     private static final int INLINE_IME_MIN_DISPLAY_SIZE = 480;
 
     private static Handler sBackgroundHandler;
 
+    private class ThreadUtils {
+        private Editable mUiEditable;
+        private Object mUiEditableReturn;
+        private Exception mUiEditableException;
+        private final SynchronousQueue<Runnable> mIcRunnableSync;
+        private final Runnable mIcSignalRunnable;
+
+        public ThreadUtils() {
+            mIcRunnableSync = new SynchronousQueue<Runnable>();
+            mIcSignalRunnable = new Runnable() {
+                @Override public void run() {
+                }
+            };
+        }
+
+        private void runOnIcThread(Handler icHandler, final Runnable runnable) {
+            if (DEBUG) {
+                GeckoApp.assertOnUiThread();
+            }
+            Runnable runner = new Runnable() {
+                @Override public void run() {
+                    try {
+                        Runnable queuedRunnable = mIcRunnableSync.take();
+                        if (DEBUG && queuedRunnable != runnable) {
+                            throw new IllegalThreadStateException("sync error");
+                        }
+                        queuedRunnable.run();
+                    } catch (InterruptedException e) {
+                    }
+                }
+            };
+            try {
+                // if we are not inside waitForUiThread(), runner will call the runnable
+                icHandler.post(runner);
+                // runnable will be called by either runner from above or waitForUiThread()
+                mIcRunnableSync.put(runnable);
+            } catch (InterruptedException e) {
+            } finally {
+                // if waitForUiThread() already called runnable, runner should not call it again
+                icHandler.removeCallbacks(runner);
+            }
+        }
+
+        public void endWaitForUiThread() {
+            if (DEBUG) {
+                GeckoApp.assertOnUiThread();
+            }
+            try {
+                mIcRunnableSync.put(mIcSignalRunnable);
+            } catch (InterruptedException e) {
+            }
+        }
+
+        public void waitForUiThread(Handler icHandler) {
+            if (DEBUG) {
+                GeckoApp.assertOnThread(icHandler.getLooper().getThread());
+            }
+            try {
+                Runnable runnable = null;
+                do {
+                    runnable = mIcRunnableSync.take();
+                    runnable.run();
+                } while (runnable != mIcSignalRunnable);
+            } catch (InterruptedException e) {
+            }
+        }
+
+        public Editable getEditableForUiThread(final Handler uiHandler,
+                                               final Handler icHandler) {
+            if (DEBUG) {
+                GeckoApp.assertOnThread(uiHandler.getLooper().getThread());
+            }
+            if (icHandler.getLooper() == uiHandler.getLooper()) {
+                // IC thread is UI thread; safe to use Editable directly
+                return getEditable();
+            }
+            // IC thread is not UI thread; we need to return a proxy Editable in order
+            // to safely use the Editable from the UI thread
+            if (mUiEditable != null) {
+                return mUiEditable;
+            }
+            final InvocationHandler invokeEditable = new InvocationHandler() {
+                @Override public Object invoke(final Object proxy,
+                                               final Method method,
+                                               final Object[] args) throws Throwable {
+                    if (DEBUG) {
+                        GeckoApp.assertOnThread(uiHandler.getLooper().getThread());
+                    }
+                    synchronized (icHandler) {
+                        // Now we are on UI thread
+                        mUiEditableReturn = null;
+                        mUiEditableException = null;
+                        // Post a Runnable that calls the real Editable and saves any
+                        // result/exception. Then wait on the Runnable to finish
+                        runOnIcThread(icHandler, new Runnable() {
+                            @Override public void run() {
+                                synchronized (icHandler) {
+                                    try {
+                                        mUiEditableReturn = method.invoke(
+                                            mEditableClient.getEditable(), args);
+                                    } catch (Exception e) {
+                                        mUiEditableException = e;
+                                    }
+                                    icHandler.notify();
+                                }
+                            }
+                        });
+                        // let InterruptedException propagate
+                        icHandler.wait();
+                        if (mUiEditableException != null) {
+                            throw mUiEditableException;
+                        }
+                        return mUiEditableReturn;
+                    }
+                }
+            };
+            mUiEditable = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(),
+                new Class<?>[] { Editable.class }, invokeEditable);
+            return mUiEditable;
+        }
+    }
+
+    private final ThreadUtils mThreadUtils = new ThreadUtils();
+
     // Managed only by notifyIMEEnabled; see comments in notifyIMEEnabled
     private int mIMEState;
     private String mIMETypeHint = "";
     private String mIMEModeHint = "";
     private String mIMEActionHint = "";
 
     private String mCurrentInputMethod = "";
 
@@ -328,17 +453,17 @@ class GeckoInputConnection
                 Looper.prepare();
                 synchronized (GeckoInputConnection.class) {
                     sBackgroundHandler = new Handler();
                     GeckoInputConnection.class.notify();
                 }
                 Looper.loop();
                 sBackgroundHandler = null;
             }
-        });
+        }, LOGTAG);
         backgroundThread.setDaemon(true);
         backgroundThread.start();
         while (sBackgroundHandler == null) {
             try {
                 // wait for new thread to set sBackgroundHandler
                 GeckoInputConnection.class.wait();
             } catch (InterruptedException e) {
             }
@@ -492,30 +617,22 @@ class GeckoInputConnection
             return false;
         }
         final Handler icHandler = mEditableClient.getInputConnectionHandler();
         final Handler mainHandler = v.getRootView().getHandler();
         if (icHandler.getLooper() != mainHandler.getLooper()) {
             // We are on separate IC thread but the event is queued on the main thread;
             // wait on IC thread until the main thread processes our posted Runnable. At
             // that point the key event has already been processed.
-            synchronized (icHandler) {
-                mainHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        synchronized (icHandler) {
-                            icHandler.notify();
-                        }
-                    }
-                });
-                try {
-                    icHandler.wait();
-                } catch (InterruptedException e) {
+            mainHandler.post(new Runnable() {
+                @Override public void run() {
+                    mThreadUtils.endWaitForUiThread();
                 }
-            }
+            });
+            mThreadUtils.waitForUiThread(icHandler);
         }
         return false; // seems to always return false
     }
 
     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
         return false;
     }
 
@@ -541,24 +658,33 @@ class GeckoInputConnection
                 break;
             default:
                 break;
         }
 
         View view = getView();
         KeyListener keyListener = TextKeyListener.getInstance();
 
-        // KeyListener returns true if it handled the event for us.
+        // KeyListener returns true if it handled the event for us. KeyListener is only
+        // safe to use on the UI thread; therefore we need to pass a proxy Editable to it
         if (mIMEState == IME_STATE_DISABLED ||
-                mIMEState == IME_STATE_PLUGIN ||
-                keyCode == KeyEvent.KEYCODE_ENTER ||
-                keyCode == KeyEvent.KEYCODE_DEL ||
-                keyCode == KeyEvent.KEYCODE_TAB ||
-                (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 ||
-                !keyListener.onKeyDown(view, getEditable(), keyCode, event)) {
+            mIMEState == IME_STATE_PLUGIN ||
+            keyCode == KeyEvent.KEYCODE_ENTER ||
+            keyCode == KeyEvent.KEYCODE_DEL ||
+            keyCode == KeyEvent.KEYCODE_TAB ||
+            (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 ||
+            view == null) {
+            mEditableClient.sendEvent(GeckoEvent.createKeyEvent(event));
+            return true;
+        }
+
+        Handler uiHandler = view.getRootView().getHandler();
+        Handler icHandler = mEditableClient.getInputConnectionHandler();
+        Editable uiEditable = mThreadUtils.getEditableForUiThread(uiHandler, icHandler);
+        if (!keyListener.onKeyDown(view, uiEditable, keyCode, event)) {
             mEditableClient.sendEvent(GeckoEvent.createKeyEvent(event));
         }
         return true;
     }
 
     public boolean onKeyUp(int keyCode, KeyEvent event) {
         return processKeyUp(keyCode, event);
     }
@@ -574,25 +700,34 @@ class GeckoInputConnection
                 return false;
             default:
                 break;
         }
 
         View view = getView();
         KeyListener keyListener = TextKeyListener.getInstance();
 
+        // KeyListener returns true if it handled the event for us. KeyListener is only
+        // safe to use on the UI thread; therefore we need to pass a proxy Editable to it
         if (mIMEState == IME_STATE_DISABLED ||
             mIMEState == IME_STATE_PLUGIN ||
             keyCode == KeyEvent.KEYCODE_ENTER ||
             keyCode == KeyEvent.KEYCODE_DEL ||
             (event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 ||
-            !keyListener.onKeyUp(view, getEditable(), keyCode, event)) {
+            view == null) {
+            mEditableClient.sendEvent(GeckoEvent.createKeyEvent(event));
+            return true;
+        }
+
+        Handler uiHandler = view.getRootView().getHandler();
+        Handler icHandler = mEditableClient.getInputConnectionHandler();
+        Editable uiEditable = mThreadUtils.getEditableForUiThread(uiHandler, icHandler);
+        if (!keyListener.onKeyUp(view, uiEditable, keyCode, event)) {
             mEditableClient.sendEvent(GeckoEvent.createKeyEvent(event));
         }
-
         return true;
     }
 
     public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
         while ((repeatCount--) != 0) {
             if (!processKeyDown(keyCode, event) ||
                 !processKeyUp(keyCode, event)) {
                 return false;