Bug 1398409 - 2. Use event callback to communicate FormAssistPopup actions; r=sebastian draft
authorJim Chen <nchen@mozilla.com>
Fri, 15 Sep 2017 14:44:50 -0400
changeset 665604 c68b6e5b8711ba021067a6bd7cbeac0d10e2f556
parent 665603 77cc5f2d0455e9d0e0b78aa6e08ad8bef8298b85
child 665605 cc7cb725f8b5eb1ab4f53bf285b6f1863f2f4d49
push id80117
push userbmo:nchen@mozilla.com
push dateFri, 15 Sep 2017 18:48:05 +0000
reviewerssebastian
bugs1398409
milestone57.0a1
Bug 1398409 - 2. Use event callback to communicate FormAssistPopup actions; r=sebastian Use event callbacks instead of separate events to deliver FormAssistPopup replies back to FormAssistant. This lets us better handle having multiple FormAssistPopup instances across Fennec, custom tabs, and PWAs. FormAssistant._currentInputElement is removed because it does not allow us to have multiple concurrent popups. Instead, we track the current element through the event callback closure. FormAssistant._currentInputValue is also removed for similar reasons, and I don't think it was really necessary. MozReview-Commit-ID: DdeMBGCxDou
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/chrome/content/FormAssistant.js
mobile/android/components/BrowserCLH.js
mobile/android/modules/geckoview/Messaging.jsm
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1635,24 +1635,16 @@ public class BrowserApp extends GeckoApp
         }
 
         for (final BrowserAppDelegate delegate : delegates) {
             delegate.onDestroy(this);
         }
 
         deleteTempFiles(getApplicationContext());
 
-        if (mDoorHangerPopup != null) {
-            mDoorHangerPopup.destroy();
-            mDoorHangerPopup = null;
-        }
-        if (mFormAssistPopup != null)
-            mFormAssistPopup.destroy();
-        if (mTextSelection != null)
-            mTextSelection.destroy();
         NotificationHelper.destroy();
         GeckoNetworkManager.destroy();
 
         super.onDestroy();
     }
 
     @Override
     protected void initializeChrome() {
--- a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
@@ -43,17 +43,19 @@ import java.util.Collection;
 public class FormAssistPopup extends RelativeLayout implements BundleEventListener {
     private final Animation mAnimation;
 
     private ListView mAutoCompleteList;
     private RelativeLayout mValidationMessage;
     private TextView mValidationMessageText;
     private ImageView mValidationMessageArrow;
     private ImageView mValidationMessageArrowInverted;
-    private final GeckoApp geckoApp;
+
+    private GeckoView mGeckoView;
+    private EventCallback mAutoCompleteCallback;
 
     private double mX;
     private double mY;
     private double mW;
     private double mH;
 
     private enum PopupType {
         AUTOCOMPLETE,
@@ -83,47 +85,39 @@ public class FormAssistPopup extends Rel
 
     public FormAssistPopup(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
         mAnimation.setDuration(75);
 
         setFocusable(false);
-
-        geckoApp = (GeckoApp) ActivityUtils.getActivityFromContext(context);
     }
 
-    @Override
-    public void onAttachedToWindow() {
-        super.onAttachedToWindow();
-
-        geckoApp.getAppEventDispatcher().registerUiThreadListener(this,
+    public void create(final GeckoView view) {
+        mGeckoView = view;
+        mGeckoView.getEventDispatcher().registerUiThreadListener(this,
             "FormAssist:AutoCompleteResult",
             "FormAssist:ValidationMessage",
             "FormAssist:Hide");
     }
 
-    void destroy() {
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        geckoApp.getAppEventDispatcher().unregisterUiThreadListener(this,
+    public void destroy() {
+        mGeckoView.getEventDispatcher().unregisterUiThreadListener(this,
             "FormAssist:AutoCompleteResult",
             "FormAssist:ValidationMessage",
             "FormAssist:Hide");
-
-        super.onDetachedFromWindow();
+        mGeckoView = null;
     }
 
     @Override // BundleEventListener
     public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback) {
         if ("FormAssist:AutoCompleteResult".equals(event)) {
+            mAutoCompleteCallback = callback;
             showAutoCompleteSuggestions(message.getBundleArray("suggestions"),
                                         message.getBundle("rect"),
                                         message.getBoolean("isEmpty"));
 
         } else if ("FormAssist:ValidationMessage".equals(event)) {
             showValidationMessage(message.getString("validationMessage"),
                                   message.getBundle("rect"));
 
@@ -144,42 +138,54 @@ public class FormAssistPopup extends Rel
         }
 
         if (mAutoCompleteList == null) {
             LayoutInflater inflater = LayoutInflater.from(getContext());
             mAutoCompleteList = (ListView) inflater.inflate(R.layout.autocomplete_list, null);
 
             mAutoCompleteList.setOnItemClickListener(new OnItemClickListener() {
                 @Override
-                public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
+                public void onItemClick(final AdapterView<?> parentView, final View view,
+                                        final int position, final long id) {
+                    if (mAutoCompleteCallback == null) {
+                        hide();
+                        return;
+                    }
+
                     // Use the value stored with the autocomplete view, not the label text,
                     // since they can be different.
                     final TextView textView = (TextView) view;
-                    final GeckoBundle message = new GeckoBundle(1);
+                    final GeckoBundle message = new GeckoBundle(2);
+                    message.putString("action", "autocomplete");
                     message.putString("value", (String) textView.getTag());
-                    geckoApp.getAppEventDispatcher().dispatch("FormAssist:AutoComplete", message);
+                    mAutoCompleteCallback.sendSuccess(message);
                     hide();
                 }
             });
 
             // Create a ListView-specific touch listener. ListViews are given special treatment because
             // by default they handle touches for their list items... i.e. they're in charge of drawing
             // the pressed state (the list selector), handling list item clicks, etc.
             final SwipeDismissListViewTouchListener touchListener = new SwipeDismissListViewTouchListener(mAutoCompleteList, new OnDismissCallback() {
                 @Override
                 public void onDismiss(ListView listView, final int position) {
+                    if (mAutoCompleteCallback == null) {
+                        return;
+                    }
+
                     // Use the value stored with the autocomplete view, not the label text,
                     // since they can be different.
                     AutoCompleteListAdapter adapter = (AutoCompleteListAdapter) listView.getAdapter();
                     Pair<String, String> item = adapter.getItem(position);
 
                     // Remove the item from form history.
-                    final GeckoBundle message = new GeckoBundle(1);
+                    final GeckoBundle message = new GeckoBundle(2);
+                    message.putString("action", "remove");
                     message.putString("value", item.second);
-                    geckoApp.getAppEventDispatcher().dispatch("FormAssist:Remove", message);
+                    mAutoCompleteCallback.sendSuccess(message);
 
                     // Update the list
                     adapter.remove(item);
                     adapter.notifyDataSetChanged();
                     positionAndShowPopup();
                 }
             });
             mAutoCompleteList.setOnTouchListener(touchListener);
@@ -243,17 +249,17 @@ public class FormAssistPopup extends Rel
         mW = rect.getDouble("w");
         mH = rect.getDouble("h");
         mPopupType = (isAutoComplete ?
                       PopupType.AUTOCOMPLETE : PopupType.VALIDATIONMESSAGE);
         return true;
     }
 
     private void positionAndShowPopup() {
-        positionAndShowPopup(GeckoAppShell.getLayerView().getViewportMetrics());
+        positionAndShowPopup(mGeckoView.getViewportMetrics());
     }
 
     private void positionAndShowPopup(ImmutableViewportMetrics aMetrics) {
         ThreadUtils.assertOnUiThread();
 
         // Don't show the form assist popup when using fullscreen VKB
         InputMethodManager imm = (InputMethodManager)
                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -276,17 +282,18 @@ public class FormAssistPopup extends Rel
             sValidationMessageHeight = (int) (res.getDimension(R.dimen.validation_message_height));
         }
 
         float zoom = aMetrics.zoomFactor;
 
         // These values correspond to the input box for which we want to
         // display the FormAssistPopup.
         int left = (int) (mX * zoom - aMetrics.viewportRectLeft);
-        int top = (int) (mY * zoom - aMetrics.viewportRectTop + GeckoAppShell.getLayerView().getCurrentToolbarHeight());
+        int top = (int) (mY * zoom - aMetrics.viewportRectTop + mGeckoView.getTop() +
+                         mGeckoView.getCurrentToolbarHeight());
         int width = (int) (mW * zoom);
         int height = (int) (mH * zoom);
 
         int popupWidth = LayoutParams.MATCH_PARENT;
         int popupLeft = left < 0 ? 0 : left;
 
         FloatSize viewport = aMetrics.getSize();
 
@@ -324,17 +331,17 @@ public class FormAssistPopup extends Rel
         if (mPopupType == PopupType.VALIDATIONMESSAGE) {
             mValidationMessageText.setLayoutParams(sValidationTextLayoutNormal);
             mValidationMessageArrow.setVisibility(VISIBLE);
             mValidationMessageArrowInverted.setVisibility(GONE);
         }
 
         // If the popup doesn't fit below the input box, shrink its height, or
         // see if we can place it above the input instead.
-        if ((popupTop + popupHeight) > viewport.height) {
+        if ((popupTop + popupHeight) > (mGeckoView.getTop() + viewport.height)) {
             // Find where the maximum space is, and put the popup there.
             if ((viewport.height - popupTop) > top) {
                 // Shrink the height to fit it below the input box.
                 popupHeight = (int) (viewport.height - popupTop);
             } else {
                 if (popupHeight < top) {
                     // No shrinking needed to fit on top.
                     popupTop = (top - popupHeight);
@@ -361,18 +368,18 @@ public class FormAssistPopup extends Rel
             setVisibility(VISIBLE);
             startAnimation(mAnimation);
         }
     }
 
     public void hide() {
         if (isShown()) {
             setVisibility(GONE);
-            geckoApp.getAppEventDispatcher().dispatch("FormAssist:Hidden", null);
         }
+        mAutoCompleteCallback = null;
     }
 
     void onTranslationChanged() {
         ThreadUtils.assertOnUiThread();
         if (!isShown()) {
             return;
         }
         positionAndShowPopup();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1478,16 +1478,17 @@ public abstract class GeckoApp extends G
         // cause a loop! See Bug 1011008, Comment 12.
         super.onConfigurationChanged(getResources().getConfiguration());
     }
 
     protected void initializeChrome() {
         mDoorHangerPopup = new DoorHangerPopup(this, getAppEventDispatcher());
         mDoorHangerPopup.setOnVisibilityChangeListener(this);
         mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
+        mFormAssistPopup.create(mLayerView);
     }
 
     @Override
     public void onDoorHangerShow() {
         final View overlay = getDoorhangerOverlay();
         if (overlay != null) {
             final Animator alphaAnimator = ObjectAnimator.ofFloat(overlay, "alpha", 1);
             alphaAnimator.setDuration(250);
@@ -2181,16 +2182,31 @@ public abstract class GeckoApp extends G
     public void onDestroy() {
         if (mIsAbortingAppLaunch) {
             // This build does not support the Android version of the device:
             // We did not initialize anything, so skip cleaning up.
             super.onDestroy();
             return;
         }
 
+        if (mFormAssistPopup != null) {
+            mFormAssistPopup.destroy();
+            mFormAssistPopup = null;
+        }
+
+        if (mDoorHangerPopup != null) {
+            mDoorHangerPopup.destroy();
+            mDoorHangerPopup = null;
+        }
+
+        if (mTextSelection != null) {
+            mTextSelection.destroy();
+            mTextSelection = null;
+        }
+
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "Accessibility:Ready",
             "Gecko:Ready",
             null);
 
         EventDispatcher.getInstance().unregisterUiThreadListener(this,
             "Update:Check",
             "Update:Download",
--- a/mobile/android/chrome/content/FormAssistant.js
+++ b/mobile/android/chrome/content/FormAssistant.js
@@ -5,80 +5,51 @@
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", "resource://gre/modules/FormHistory.jsm");
 
 var FormAssistant = {
   // Weak-ref used to keep track of the currently focused element.
   _currentFocusedElement: null,
 
-  // Used to keep track of the element that corresponds to the current
-  // autocomplete suggestions.
-  _currentInputElement: null,
-
-  // The value of the currently focused input.
-  _currentInputValue: null,
-
   // Whether we're in the middle of an autocomplete.
   _doingAutocomplete: false,
 
   init: function() {
     Services.obs.addObserver(this, "PanZoom:StateChange");
   },
 
-  register: function(aWindow) {
-    GeckoViewUtils.getDispatcherForWindow(aWindow).registerListener(this, [
-      "FormAssist:AutoComplete",
-      "FormAssist:Hidden",
-      "FormAssist:Remove",
-    ]);
-  },
-
-  onEvent: function(event, message, callback) {
-    switch (event) {
-      case "FormAssist:AutoComplete": {
-        if (!this._currentInputElement) {
-          break;
-        }
-        let editableElement = this._currentInputElement.QueryInterface(
-            Ci.nsIDOMNSEditableElement);
+  _onPopupResponse: function(currentElement, message) {
+    switch (message.action) {
+      case "autocomplete": {
+        let editableElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement);
         this._doingAutocomplete = true;
 
         // If we have an active composition string, commit it before sending
         // the autocomplete event with the text that will replace it.
         try {
           if (editableElement.editor.composing) {
             editableElement.editor.forceCompositionEnd();
           }
         } catch (e) {}
 
         editableElement.setUserInput(message.value);
-        this._currentInputValue = message.value;
 
-        let event = this._currentInputElement.ownerDocument.createEvent("Events");
+        let event = currentElement.ownerDocument.createEvent("Events");
         event.initEvent("DOMAutoComplete", true, true);
-        this._currentInputElement.dispatchEvent(event);
+        currentElement.dispatchEvent(event);
 
         this._doingAutocomplete = false;
         break;
       }
 
-      case "FormAssist:Hidden": {
-        this._currentInputElement = null;
-        break;
-      }
-
-      case "FormAssist:Remove": {
-        if (!this._currentInputElement) {
-          break;
-        }
-
+      case "remove": {
         FormHistory.update({
           op: "remove",
-          fieldname: this._currentInputElement.name,
+          fieldname: currentElement.name,
           value: message.value,
         });
         break;
       }
     }
   },
 
   observe: function(aSubject, aTopic, aData) {
@@ -131,17 +102,16 @@ var FormAssistant = {
         if (this._showValidationMessage(currentElement) ||
             this._isAutoComplete(currentElement)) {
           this._currentFocusedElement = Cu.getWeakReference(currentElement);
         }
         break;
       }
 
       case "blur": {
-        this._currentInputValue = null;
         this._currentFocusedElement = null;
         break;
       }
 
       case "click": {
         let currentElement = aEvent.target;
 
         // Prioritize a form validation message over autocomplete suggestions
@@ -163,24 +133,20 @@ var FormAssistant = {
 
       case "input": {
         let currentElement = aEvent.target;
         let focused = this._currentFocusedElement && this._currentFocusedElement.get();
 
         // If this element isn't focused, we're already in middle of an
         // autocomplete, or its value hasn't changed, don't show the
         // autocomplete popup.
-        if (currentElement !== focused ||
-            this._doingAutocomplete ||
-            currentElement.value === this._currentInputValue) {
+        if (currentElement !== focused || this._doingAutocomplete) {
           break;
         }
 
-        this._currentInputValue = currentElement.value;
-
         // Since we can only show one popup at a time, prioritze autocomplete
         // suggestions over a form validation message
         let checkResultsInput = hasResults => {
           if (hasResults || this._showValidationMessage(currentElement)) {
             return;
           }
           // If we're not showing autocomplete suggestions, hide the form assist popup
           this._hideFormAssistPopup(currentElement);
@@ -290,21 +256,21 @@ var FormAssistant = {
         return;
       }
 
       GeckoViewUtils.getDispatcherForWindow(aElement.ownerGlobal).sendRequest({
         type: "FormAssist:AutoCompleteResult",
         suggestions: suggestions,
         rect: this._getBoundingContentRect(aElement),
         isEmpty: isEmpty,
+      }, {
+        onSuccess: response => this._onPopupResponse(aElement, response),
+        onError: error => Cu.reportError(error),
       });
 
-      // Keep track of input element so we can fill it in if the user
-      // selects an autocomplete suggestion
-      this._currentInputElement = aElement;
       aCallback(true);
     };
 
     this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
   },
 
   // Only show a validation message if the user submitted an invalid form,
   // there's a non-empty message string, and the element is the correct type
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -137,17 +137,16 @@ BrowserCLH.prototype = {
           "focus", "blur", "click", "input",
         ], {
           handler: event => {
             if (event.target instanceof Ci.nsIDOMHTMLInputElement ||
                 event.target instanceof Ci.nsIDOMHTMLTextAreaElement ||
                 event.target instanceof Ci.nsIDOMHTMLSelectElement ||
                 event.target instanceof Ci.nsIDOMHTMLButtonElement) {
               // Only load FormAssistant when the event target is what we care about.
-              this.FormAssistant.register(win);
               return this.FormAssistant;
             }
             return null;
           },
           options: {
             capture: true,
             mozSystemGroup: true,
           },
--- a/mobile/android/modules/geckoview/Messaging.jsm
+++ b/mobile/android/modules/geckoview/Messaging.jsm
@@ -108,20 +108,20 @@ DispatcherDelegate.prototype = {
    * Implementations of Messaging APIs for backwards compatibility.
    */
 
   /**
    * Sends a request to Java.
    *
    * @param msg Message to send; must be an object with a "type" property
    */
-  sendRequest: function(msg) {
+  sendRequest: function(msg, callback) {
     let type = msg.type;
     msg.type = undefined;
-    this.dispatch(type, msg);
+    this.dispatch(type, msg, callback);
   },
 
   /**
    * Sends a request to Java, returning a Promise that resolves to the response.
    *
    * @param msg Message to send; must be an object with a "type" property
    * @returns A Promise resolving to the response
    */