Bug 1442250 - 2. Unregister then re-register all listeners on close; r?esawin draft
authorJim Chen <nchen@mozilla.com>
Fri, 09 Mar 2018 12:34:37 -0500
changeset 765368 f96da9d5ccf565852fa93223f3c85c27411a2466
parent 765367 31c51a65100f46a2c659e0578cc0962deeb97163
child 765369 4c4a89e5c212dc8848a58ee80cf077dc7952f886
push id102052
push userbmo:nchen@mozilla.com
push dateFri, 09 Mar 2018 17:35:14 +0000
reviewersesawin
bugs1442250
milestone60.0a1
Bug 1442250 - 2. Unregister then re-register all listeners on close; r?esawin Ensure that we unregister and re-register all listeners on closeWindow, so that the listeners are ready for any new windows that are subsequently opened. MozReview-Commit-ID: EKzCRS10odN
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java
widget/android/GeneratedJNIWrappers.h
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -41,17 +41,27 @@ import android.support.annotation.Nullab
 import android.support.annotation.NonNull;
 import android.util.Log;
 
 public class GeckoSession extends LayerSession
                           implements Parcelable {
     private static final String LOGTAG = "GeckoSession";
     private static final boolean DEBUG = false;
 
-    /* package */ enum State implements NativeQueue.State {
+    // Type of changes given to onWindowChanged.
+    // Window has been cleared due to the session being closed.
+    private static final int WINDOW_CLOSE = 0;
+    // Window has been set due to the session being opened.
+    private static final int WINDOW_OPEN = 1; // Window has been opened.
+    // Window has been cleared due to the session being transferred to another session.
+    private static final int WINDOW_TRANSFER_OUT = 2; // Window has been transfer.
+    // Window has been set due to another session being transferred to this one.
+    private static final int WINDOW_TRANSFER_IN = 3;
+
+    private enum State implements NativeQueue.State {
         INITIAL(0),
         READY(1);
 
         private final int mRank;
 
         private State(int rank) {
             mRank = rank;
         }
@@ -304,16 +314,23 @@ public class GeckoSession extends LayerS
 
                     delegate.onMediaPermissionRequest(
                             GeckoSession.this, message.getString("uri"),
                             videos, audios, new PermissionCallback("media", callback));
                 }
             }
         };
 
+    /* package */ int handlersCount;
+
+    private final GeckoSessionHandler<?>[] mSessionHandlers = new GeckoSessionHandler<?>[] {
+        mContentHandler, mNavigationHandler, mProgressHandler, mScrollHandler,
+        mTrackingProtectionHandler, mPermissionHandler
+    };
+
     private static class PermissionCallback implements
         PermissionDelegate.Callback, PermissionDelegate.MediaCallback {
 
         private final String mType;
         private EventCallback mCallback;
 
         public PermissionCallback(final String type, final EventCallback callback) {
             mType = type;
@@ -397,64 +414,79 @@ public class GeckoSession extends LayerS
 
         @WrapForJNI(dispatchTo = "proxy")
         public static native void open(Window instance, Compositor compositor,
                                        EventDispatcher dispatcher,
                                        GeckoBundle settings, String chromeUri,
                                        int screenId, boolean privateMode, String id);
 
         @Override // JNIObject
-        protected void disposeNative() {
-            // Detach ourselves from the binder as well, to prevent this window from being
-            // read from any parcels.
-            asBinder().attachInterface(null, Window.class.getName());
-
-            // Reset our queue, so we don't end up with queued calls on a disposed object.
-            synchronized (this) {
-                mNativeQueue.reset(State.INITIAL);
-            }
-
+        public void disposeNative() {
             if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
                 nativeDisposeNative();
             } else {
                 GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
                         this, "nativeDisposeNative");
             }
         }
 
         @WrapForJNI(dispatchTo = "proxy", stubName = "DisposeNative")
         private native void nativeDisposeNative();
 
-        @WrapForJNI(dispatchTo = "proxy")
-        public native void close();
+        public void close() {
+            // Reset our queue, so we don't end up with queued calls on a disposed object.
+            synchronized (this) {
+                if (mNativeQueue == null) {
+                    // Already closed elsewhere.
+                    return;
+                }
+                mNativeQueue.reset(State.INITIAL);
+                mNativeQueue = null;
+            }
+
+            // Detach ourselves from the binder as well, to prevent this window from being
+            // read from any parcels.
+            asBinder().attachInterface(null, Window.class.getName());
+
+            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+                nativeClose();
+            } else {
+                GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
+                        this, "nativeClose");
+            }
+        }
+
+        @WrapForJNI(dispatchTo = "proxy", stubName = "Close")
+        private native void nativeClose();
 
         @WrapForJNI(dispatchTo = "proxy")
         public native void transfer(Compositor compositor, EventDispatcher dispatcher,
                                     GeckoBundle settings);
 
         @WrapForJNI(calledFrom = "gecko")
         private synchronized void onTransfer(final EventDispatcher dispatcher) {
             final NativeQueue nativeQueue = dispatcher.getNativeQueue();
-            if (mNativeQueue != nativeQueue) {
+            if (mNativeQueue != null && mNativeQueue != nativeQueue) {
                 // Set new queue to the same state as the old queue,
                 // then return the old queue to its initial state if applicable,
                 // because the old queue is no longer the active queue.
                 nativeQueue.setState(mNativeQueue.getState());
                 mNativeQueue.reset(State.INITIAL);
                 mNativeQueue = nativeQueue;
             }
         }
 
         @WrapForJNI(dispatchTo = "proxy")
         public native void attachEditable(IGeckoEditableParent parent,
                                           GeckoEditableChild child);
 
         @WrapForJNI(calledFrom = "gecko")
         private synchronized void onReady() {
-            if (mNativeQueue.checkAndSetState(State.INITIAL, State.READY)) {
+            if (mNativeQueue != null &&
+                    mNativeQueue.checkAndSetState(State.INITIAL, State.READY)) {
                 Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
                       " - chrome startup finished");
             }
         }
     }
 
     private class Listener implements BundleEventListener {
         /* package */ void registerListeners() {
@@ -481,47 +513,63 @@ public class GeckoSession extends LayerS
 
     public GeckoSession() {
         this(null);
     }
 
     public GeckoSession(final GeckoSessionSettings settings) {
         mSettings = new GeckoSessionSettings(settings, this);
         mListener.registerListeners();
+
+        if (BuildConfig.DEBUG && handlersCount != mSessionHandlers.length) {
+            throw new AssertionError("Add new handler to handlers list");
+        }
     }
 
     private void transferFrom(final Window window, final GeckoSessionSettings settings,
                               final String id) {
         if (isOpen()) {
             throw new IllegalStateException("Session is open");
         }
 
+        if (window != null) {
+            onWindowChanged(WINDOW_TRANSFER_IN, /* inProgress */ true);
+        }
+
         mWindow = window;
         mSettings = new GeckoSessionSettings(settings, this);
         mId = id;
 
         if (mWindow != null) {
             if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
                 mWindow.transfer(mCompositor, mEventDispatcher, mSettings.asBundle());
             } else {
                 GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
                         mWindow, "transfer",
                         Compositor.class, mCompositor,
                         EventDispatcher.class, mEventDispatcher,
                         GeckoBundle.class, mSettings.asBundle());
             }
+
+            onWindowChanged(WINDOW_TRANSFER_IN, /* inProgress */ false);
         }
-
-        onWindowChanged();
     }
 
     /* package */ void transferFrom(final GeckoSession session) {
+        final boolean changing = (session.mWindow != null);
+        if (changing) {
+            session.onWindowChanged(WINDOW_TRANSFER_OUT, /* inProgress */ true);
+        }
+
         transferFrom(session.mWindow, session.mSettings, session.mId);
         session.mWindow = null;
-        session.onWindowChanged();
+
+        if (changing) {
+            session.onWindowChanged(WINDOW_TRANSFER_OUT, /* inProgress */ false);
+        }
     }
 
     @Override // Parcelable
     public int describeContents() {
         return 0;
     }
 
     @Override // Parcelable
@@ -633,32 +681,34 @@ public class GeckoSession extends LayerS
 
     private void openWindow() {
         final String chromeUri = mSettings.getString(GeckoSessionSettings.CHROME_URI);
         final int screenId = mSettings.getInt(GeckoSessionSettings.SCREEN_ID);
         final boolean isPrivate = mSettings.getBoolean(GeckoSessionSettings.USE_PRIVATE_MODE);
 
         mWindow = new Window(mNativeQueue);
 
+        onWindowChanged(WINDOW_OPEN, /* inProgress */ true);
+
         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
             Window.open(mWindow, mCompositor, mEventDispatcher,
                         mSettings.asBundle(), chromeUri, screenId, isPrivate, mId);
         } else {
             GeckoThread.queueNativeCallUntil(
                 GeckoThread.State.PROFILE_READY,
                 Window.class, "open",
                 Window.class, mWindow,
                 Compositor.class, mCompositor,
                 EventDispatcher.class, mEventDispatcher,
                 GeckoBundle.class, mSettings.asBundle(),
                 String.class, chromeUri,
                 screenId, isPrivate, mId);
         }
 
-        onWindowChanged();
+        onWindowChanged(WINDOW_OPEN, /* inProgress */ false);
     }
 
     /**
      * Closes the session.
      *
      * This frees the underlying Gecko objects and unloads the current page. The session may be
      * reopened later, but page state is not restored. Call this when you are finished using
      * a GeckoSession instance.
@@ -666,31 +716,37 @@ public class GeckoSession extends LayerS
     public void close() {
         ThreadUtils.assertOnUiThread();
 
         if (!isOpen()) {
             Log.w(LOGTAG, "Attempted to close a GeckoSession that was already closed.");
             return;
         }
 
-        if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-            mWindow.close();
-        } else {
-            GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
-                    mWindow, "close");
+        onWindowChanged(WINDOW_CLOSE, /* inProgress */ true);
+
+        mWindow.close();
+        mWindow.disposeNative();
+        mWindow = null;
+
+        onWindowChanged(WINDOW_CLOSE, /* inProgress */ false);
+    }
+
+    private void onWindowChanged(int change, boolean inProgress) {
+        if ((change == WINDOW_OPEN || change == WINDOW_TRANSFER_IN) && !inProgress) {
+            mTextInput.onWindowChanged(mWindow);
         }
 
-        mWindow.disposeNative();
-        mWindow = null;
-        onWindowChanged();
-    }
-
-    private void onWindowChanged() {
-        if (mWindow != null) {
-            mTextInput.onWindowChanged(mWindow);
+        if (change == WINDOW_CLOSE) {
+            // Detach when window is closing, and reattach immediately after window is closed.
+            // We reattach immediate after closing because we want any actions performed while the
+            // session is closed to be properly queued, until the session is open again.
+            for (final GeckoSessionHandler<?> handler : mSessionHandlers) {
+                handler.setSessionIsReady(getEventDispatcher(), !inProgress);
+            }
         }
     }
 
     /**
      * Get the TextInputController instance for this session.
      *
      * @return TextInputController instance.
      */
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSessionHandler.java
@@ -5,17 +5,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview;
 
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
-import org.mozilla.geckoview.GeckoSession;
 
 import android.util.Log;
 
 /* package */ abstract class GeckoSessionHandler<Delegate>
     implements BundleEventListener {
 
     private static final String LOGTAG = "GeckoSessionHandler";
     private static final boolean DEBUG = false;
@@ -31,71 +30,86 @@ import android.util.Log;
                                       final String[] events) {
         this(module, session, events, /* alwaysListen */ false);
     }
 
     /* package */ GeckoSessionHandler(final String module,
                                       final GeckoSession session,
                                       final String[] events,
                                       final boolean alwaysListen) {
+        session.handlersCount++;
+
         mAlwaysListen = alwaysListen;
         mModuleName = module;
         mEvents = events;
 
         if (alwaysListen) {
             register(session.getEventDispatcher());
+            setSessionIsReady(session.getEventDispatcher(), /* ready */ true);
         }
     }
 
     public Delegate getDelegate() {
         return mDelegate;
     }
 
     public void setDelegate(final Delegate delegate, final GeckoSession session) {
         final EventDispatcher eventDispatcher = session.getEventDispatcher();
         if (mDelegate == delegate) {
             return;
         }
 
-        if (!mAlwaysListen && mDelegate != null) {
+        final boolean unsettingOldDelegate = mDelegate != null &&
+                                             delegate == null;
+        final boolean settingNewDelegate = mDelegate == null &&
+                                           delegate != null;
+
+        if (!mAlwaysListen && unsettingOldDelegate) {
             unregister(eventDispatcher);
         }
 
         mDelegate = delegate;
 
-        if (!mAlwaysListen && mDelegate != null) {
+        if (!mAlwaysListen && settingNewDelegate) {
             register(eventDispatcher);
         }
     }
 
     private void unregister(final EventDispatcher eventDispatcher) {
-        final GeckoBundle msg = new GeckoBundle(1);
-        msg.putString("module", mModuleName);
-        eventDispatcher.dispatch("GeckoView:Unregister", msg);
+        setSessionIsReady(eventDispatcher, /* ready */ false);
         eventDispatcher.unregisterUiThreadListener(this, mEvents);
     }
 
     private void register(final EventDispatcher eventDispatcher) {
+        eventDispatcher.registerUiThreadListener(this, mEvents);
+        setSessionIsReady(eventDispatcher, /* ready */ true);
+    }
+
+    public void setSessionIsReady(final EventDispatcher eventDispatcher, final boolean ready) {
+        if (!mAlwaysListen && mDelegate == null) {
+            return;
+        }
+
         final GeckoBundle msg = new GeckoBundle(1);
         msg.putString("module", mModuleName);
-        eventDispatcher.dispatch("GeckoView:Register", msg);
-        eventDispatcher.registerUiThreadListener(this, mEvents);
+        eventDispatcher.dispatch(ready ? "GeckoView:Register"
+                                       : "GeckoView:Unregister", msg);
     }
 
     @Override
     public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback) {
         if (DEBUG) {
             Log.d(LOGTAG, mModuleName + " handleMessage: event = " + event);
         }
 
         if (mDelegate != null) {
             handleMessage(mDelegate, event, message, callback);
         } else {
-            callback.sendError("No listener registered");
+            callback.sendError("No delegate registered");
         }
     }
 
     protected abstract void handleMessage(final Delegate delegate,
                                           final String event,
                                           final GeckoBundle message,
                                           final EventCallback callback);
 }
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -7144,17 +7144,17 @@ public:
                 mozilla::jni::DispatchTarget::PROXY;
     };
 
     struct Close_t {
         typedef Window Owner;
         typedef void ReturnType;
         typedef void SetterType;
         typedef mozilla::jni::Args<> Args;
-        static constexpr char name[] = "close";
+        static constexpr char name[] = "nativeClose";
         static constexpr char signature[] =
                 "()V";
         static const bool isStatic = false;
         static const mozilla::jni::ExceptionMode exceptionMode =
                 mozilla::jni::ExceptionMode::ABORT;
         static const mozilla::jni::CallingThread callingThread =
                 mozilla::jni::CallingThread::ANY;
         static const mozilla::jni::DispatchTarget dispatchTarget =