Bug 1367079 - 4. Move ActionBarHandler usage out of browser.js; r=snorp draft
authorJim Chen <nchen@mozilla.com>
Fri, 15 Sep 2017 14:44:51 -0400
changeset 665615 456512bb53b3b188a42bee42cb94c5fb4e115e07
parent 665614 9cf3a720a6dee0581e9340fd5ead20ac2bd53498
child 665616 62551fed32177d21a487f8c7fcebd56de0fc90af
push id80120
push userbmo:nchen@mozilla.com
push dateFri, 15 Sep 2017 18:49:36 +0000
reviewerssnorp
bugs1367079
milestone57.0a1
Bug 1367079 - 4. Move ActionBarHandler usage out of browser.js; r=snorp Use ActionBarHandler in BrowserCLH.js instead of browser.js, so it can handle text selection for all windows. Also update ActionBarHandler to reflect the new usage and to support multiple windows. MozReview-Commit-ID: G8sKu2XyAAG
mobile/android/chrome/content/ActionBarHandler.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/geckoview/geckoview.js
mobile/android/components/BrowserCLH.js
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -1,19 +1,25 @@
 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+  Snackbars: "resource://gre/modules/Snackbars.jsm",
+  UITelemetry: "resource://gre/modules/UITelemetry.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+  "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
 
 const PHONE_REGEX = /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/; // Are we a phone #?
 
-
 /**
  * ActionBarHandler Object and methods. Interface between Gecko Text Selection code
  * (AccessibleCaret, etc) and the Mobile ActionBar UI.
  */
 var ActionBarHandler = {
   // Error codes returned from _init().
   START_TOUCH_ERROR: {
     NO_CONTENT_WINDOW: "No valid content Window found.",
@@ -25,17 +31,17 @@ var ActionBarHandler = {
 
   _boundingClientRect: null, // Current selections boundingClientRect.
   _actionBarActions: null, // Most-recent set of actions sent to ActionBar.
 
   /**
    * Receive and act on AccessibleCarets caret state-change
    * (mozcaretstatechanged) events.
    */
-  caretStateChangedHandler: function(e) {
+  handleEvent: function(e) {
     // Close an open ActionBar, if carets no longer logically visible.
     if (this._selectionID && !e.caretVisible) {
       this._uninit(false);
       return;
     }
 
     if (!this._selectionID && e.collapsed) {
       switch (e.reason) {
@@ -122,45 +128,59 @@ var ActionBarHandler = {
         if (this._selectionID == data.selectionID) {
           this._uninit();
         }
         break;
       }
     }
   },
 
+  _getDispatcher: function(win) {
+    try {
+      return GeckoViewUtils.getDispatcherForWindow(win);
+    } catch (e) {
+      return null;
+    }
+  },
+
   /**
    * Called when Gecko AccessibleCaret becomes visible.
    */
   _init: function(boundingClientRect) {
     let [element, win] = this._getSelectionTargets();
-    if (!win) {
+    let dispatcher = this._getDispatcher(win);
+    if (!win || !dispatcher) {
       return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW;
     }
 
     // Hold the ActionBar ID provided by Gecko.
     this._selectionID = this._nextSelectionID++;
     [this._targetElement, this._contentWindow] = [element, win];
     this._boundingClientRect = boundingClientRect;
 
     // Open the ActionBar, send it's actions list.
-    WindowEventDispatcher.sendRequest({
+    dispatcher.sendRequest({
       type: "TextSelection:ActionbarInit",
       selectionID: this._selectionID,
     });
     this._sendActionBarActions(true);
 
     return this.START_TOUCH_ERROR.NONE;
   },
 
   /**
    * Called when content is scrolled and handles are hidden.
    */
   _updateVisibility: function() {
-    WindowEventDispatcher.sendRequest({
+    let win = this._contentWindow;
+    let dispatcher = this._getDispatcher(win);
+    if (!dispatcher) {
+      return;
+    }
+    dispatcher.sendRequest({
       type: "TextSelection:Visibility",
       selectionID: this._selectionID,
     });
   },
 
   /**
    * Determines the window containing the selection, and its
    * editable element if present.
@@ -168,18 +188,18 @@ var ActionBarHandler = {
   _getSelectionTargets: function() {
     let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow];
     if (!element) {
       // No focused editable.
       return [null, win];
     }
 
     // Return focused editable text element and its window.
-    if (((element instanceof HTMLInputElement) && element.mozIsTextField(false)) ||
-        (element instanceof HTMLTextAreaElement) ||
+    if (((element instanceof Ci.nsIDOMHTMLInputElement) && element.mozIsTextField(false)) ||
+        (element instanceof Ci.nsIDOMHTMLTextAreaElement) ||
         element.isContentEditable) {
       return [element, win];
     }
 
     // Focused element can't contain text.
     return [null, win];
   },
 
@@ -200,20 +220,24 @@ var ActionBarHandler = {
    * methods such as SELECT_ALL, PASTE, etc.
    */
   _uninit: function(clearSelection = true) {
     // Bail if there's no active selection.
     if (!this._selectionID) {
       return;
     }
 
-    // Close the ActionBar.
-    WindowEventDispatcher.sendRequest({
-      type: "TextSelection:ActionbarUninit",
-    });
+    let win = this._contentWindow;
+    let dispatcher = this._getDispatcher(win);
+    if (dispatcher) {
+      // Close the ActionBar.
+      dispatcher.sendRequest({
+        type: "TextSelection:ActionbarUninit",
+      });
+    }
 
     // Clear the selection ID to complete the uninit(), but leave our reference
     // to selectionTargets (_targetElement, _contentWindow) in case we need
     // a final clearSelection().
     this._selectionID = null;
     this._boundingClientRect = null;
 
     // Clear selection required if triggered by self, or TextSelection icon
@@ -259,18 +283,24 @@ var ActionBarHandler = {
 
     let actionCountUnchanged = this._actionBarActions &&
       actions.length === this._actionBarActions.length;
     let actionsMatch = actionCountUnchanged &&
       this._actionBarActions.every((e, i) => {
         return e.id === actions[i].id;
       });
 
+    let win = this._contentWindow;
+    let dispatcher = this._getDispatcher(win);
+    if (!dispatcher) {
+      return;
+    }
+
     if (sendAlways || !actionsMatch) {
-      WindowEventDispatcher.sendRequest({
+      dispatcher.sendRequest({
         type: "TextSelection:ActionbarStatus",
         selectionID: this._selectionID,
         actions: actions,
         x: this._boundingClientRect.x,
         y: this._boundingClientRect.y,
         width: this._boundingClientRect.width,
         height: this._boundingClientRect.height
       });
@@ -478,18 +508,29 @@ var ActionBarHandler = {
 
       selector: {
         matches: function(element, win) {
           return (ActionBarHandler._getSelectedPhoneNumber() != null);
         },
       },
 
       action: function(element, win) {
-        BrowserApp.loadURI("tel:" +
-          ActionBarHandler._getSelectedPhoneNumber());
+        let uri = "tel:" + ActionBarHandler._getSelectedPhoneNumber();
+        let chrome = GeckoViewUtils.getChromeWindow(win);
+        if (chrome.BrowserApp && chrome.BrowserApp.loadURI) {
+          chrome.BrowserApp.loadURI(uri);
+        } else {
+          let bwin = chrome.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow;
+          if (bwin) {
+            bwin.openURI(Services.io.newURI(uri), win,
+                         Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+                         Ci.nsIBrowserDOMWindow.OPEN_NEW,
+                         win.document.nodePrincipal);
+          }
+        }
 
         ActionBarHandler._uninit();
         UITelemetry.addEvent("action.1", "actionbar", null, "call");
       },
     },
 
     SEARCH: {
       id: "search_action",
@@ -508,67 +549,83 @@ var ActionBarHandler = {
 
       action: function(element, win) {
         let selectedText = ActionBarHandler._getSelectedText();
         ActionBarHandler._uninit();
 
         // Set current tab as parent of new tab,
         // and set new tab as private if the parent is.
         let searchSubmission = Services.search.defaultEngine.getSubmission(selectedText);
-        let parent = BrowserApp.selectedTab;
-        let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
-        BrowserApp.addTab(searchSubmission.uri.spec,
-          { parentId: parent.id,
-            selected: true,
-            isPrivate: isPrivate,
+        let chrome = GeckoViewUtils.getChromeWindow(win);
+        if (chrome.BrowserApp && chrome.BrowserApp.selectedTab && chrome.BrowserApp.addTab) {
+          let parent = chrome.BrowserApp.selectedTab;
+          let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+          chrome.BrowserApp.addTab(searchSubmission.uri.spec,
+            { parentId: parent.id,
+              selected: true,
+              isPrivate: isPrivate,
+            }
+          );
+        } else {
+          let bwin = chrome.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow;
+          if (bwin) {
+            bwin.openURI(searchSubmission.uri, win,
+                         Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+                         Ci.nsIBrowserDOMWindow.OPEN_NEW,
+                         win.document.nodePrincipal);
           }
-        );
+        }
 
         UITelemetry.addEvent("action.1", "actionbar", null, "search");
       },
     },
 
     SEARCH_ADD: {
       id: "search_add_action",
       label: () => Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
       icon: "drawable://ab_add_search_engine",
       order: 0,
       floatingOrder: 8,
 
       selector: {
         matches: function(element, win) {
-          if (!(element instanceof HTMLInputElement)) {
+          let chrome = GeckoViewUtils.getChromeWindow(win);
+          if (!chrome.SearchEngines) {
+            return false;
+          }
+          if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
             return false;
           }
           let form = element.form;
           if (!form || element.type == "password") {
             return false;
           }
 
           let method = form.method.toUpperCase();
           let canAddEngine = (method == "GET") ||
             (method == "POST" && (form.enctype != "text/plain" && form.enctype != "multipart/form-data"));
           if (!canAddEngine) {
             return false;
           }
 
           // If SearchEngine query finds it, then we don't want action to add displayed.
-          if (SearchEngines.visibleEngineExists(element)) {
+          if (chrome.SearchEngines.visibleEngineExists(element)) {
             return false;
           }
 
           return true;
         },
       },
 
       action: function(element, win) {
         UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
 
         // Engines are added asynch. If required, update SelectionUI on callback.
-        SearchEngines.addEngine(element, (result) => {
+        let chrome = GeckoViewUtils.getChromeWindow(win);
+        chrome.SearchEngines.addEngine(element, (result) => {
           if (result) {
             ActionBarHandler._sendActionBarActions(true);
           }
         });
       },
     },
 
     SHARE: {
@@ -584,19 +641,26 @@ var ActionBarHandler = {
             return false;
           }
           // Allow if selected text exists.
           return (ActionBarHandler._getSelectedText().length > 0);
         },
       },
 
       action: function(element, win) {
-        WindowEventDispatcher.sendRequest({
+        let title = win.document.title;
+        if (title && title.length > 200) {
+          title = title.slice(0, 200) + "\u2026"; // Add ellipsis.
+        } else if (!title) {
+          title = win.location.href;
+        }
+        EventDispatcher.instance.sendRequest({
           type: "Share:Text",
           text: ActionBarHandler._getSelectedText(),
+          title: title,
         });
 
         ActionBarHandler._uninit();
         UITelemetry.addEvent("action.1", "actionbar", null, "share");
       },
     },
   },
 
@@ -653,17 +717,18 @@ var ActionBarHandler = {
     // is no active selection.
     if (!this._selectionID) {
       return "";
     }
 
     let selection = this._getSelection();
 
     // Textarea can contain LF, etc.
-    if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
+    if (this._targetElement &&
+        this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
       let flags = Ci.nsIDocumentEncoder.OutputPreformatted |
         Ci.nsIDocumentEncoder.OutputRaw;
       return selection.QueryInterface(Ci.nsISelectionPrivate).
         toStringWithFormat("text/plain", flags, 0);
     }
 
     // Return explicitly selected text.
     return selection.toString();
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -207,19 +207,16 @@ lazilyLoadedObserverScripts.forEach(func
   messages.forEach((message) => {
     let [name, listenAfterClose] = message;
     mm.addMessageListener(name, listener, listenAfterClose);
   });
 });
 
 // Lazily-loaded JS subscripts and modules that use global/window EventDispatcher.
 [
-  ["ActionBarHandler", WindowEventDispatcher,
-   ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
-   "chrome://browser/content/ActionBarHandler.js"],
   ["EmbedRT", WindowEventDispatcher,
    ["GeckoView:ImportScript"],
    "chrome://browser/content/EmbedRT.js"],
   ["Feedback", GlobalEventDispatcher,
    ["Feedback:Show"],
    "chrome://browser/content/Feedback.js"],
   ["FeedHandler", GlobalEventDispatcher,
    ["Feeds:Subscribe"],
@@ -539,27 +536,21 @@ var BrowserApp = {
       // Collect telemetry data.
       // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
       InitLater(() => {
         Telemetry.addData("FENNEC_TRACKING_PROTECTION_STATE", parseInt(BrowserApp.getTrackingProtectionState()));
       });
 
       InitLater(() => LightWeightThemeWebInstaller.init());
       InitLater(() => CastingApps.init(), window, "CastingApps");
-      InitLater(() => Services.search.init(), Services, "search");
 
       // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
       InitLater(() => SafeBrowsing.init(), window, "SafeBrowsing");
 
     }, {once: true});
-
-    // Pass caret StateChanged events to ActionBarHandler.
-    window.addEventListener("mozcaretstatechanged", e => {
-      ActionBarHandler.caretStateChangedHandler(e);
-    }, /* useCapture = */ true, /* wantsUntrusted = */ false);
   },
 
   get _startupStatus() {
     delete this._startupStatus;
 
     let savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone", "");
     let ourMilestone = AppConstants.MOZ_APP_VERSION;
     this._startupStatus = "";
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -59,9 +59,13 @@ function startup() {
   ModuleManager.add("resource://gre/modules/GeckoViewContent.jsm",
                     "GeckoViewContent");
   ModuleManager.add("resource://gre/modules/GeckoViewProgress.jsm",
                     "GeckoViewProgress");
   ModuleManager.add("resource://gre/modules/GeckoViewScroll.jsm",
                     "GeckoViewScroll");
   ModuleManager.add("resource://gre/modules/GeckoViewTab.jsm",
                     "GeckoViewTab");
+
+  // Move focus to the content window at the end of startup,
+  // so things like text selection can work properly.
+  document.getElementById("content").focus();
 }
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -4,16 +4,17 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   DelayedInit: "resource://gre/modules/DelayedInit.jsm",
+  EventDispatcher: "resource://gre/modules/Messaging.jsm",
   GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
 var Strings = {};
 
 XPCOMUtils.defineLazyGetter(Strings, "brand", _ =>
         Services.strings.createBundle("chrome://branding/locale/brand.properties"));
@@ -41,18 +42,18 @@ BrowserCLH.prototype = {
     protocolHandler.setSubstitution("android", Services.io.newURI(url));
   },
 
   observe: function(subject, topic, data) {
     switch (topic) {
       case "app-startup": {
         this.setResourceSubstitutions();
 
-        Services.obs.addObserver(this, "chrome-document-global-created");
-        Services.obs.addObserver(this, "content-document-global-created");
+        Services.obs.addObserver(this, "chrome-document-interactive");
+        Services.obs.addObserver(this, "content-document-interactive");
 
         GeckoViewUtils.addLazyGetter(this, "DownloadNotifications", {
           module: "resource://gre/modules/DownloadNotifications.jsm",
           observers: ["chrome-document-loaded"],
           once: true,
         });
 
         if (AppConstants.MOZ_WEBRTC) {
@@ -100,32 +101,39 @@ BrowserCLH.prototype = {
             "RemoteLogins:insecureLoginFormPresent",
             // PLEASE KEEP THIS LIST IN SYNC WITH THE DESKTOP LIST IN nsBrowserGlue.js
           ],
         });
         GeckoViewUtils.addLazyGetter(this, "LoginManagerContent", {
           module: "resource://gre/modules/LoginManagerContent.jsm",
         });
 
+        GeckoViewUtils.addLazyGetter(this, "ActionBarHandler", {
+          script: "chrome://browser/content/ActionBarHandler.js",
+        });
+
         // Once the first chrome window is loaded, schedule a list of startup
         // tasks to be performed on idle.
         GeckoViewUtils.addLazyGetter(this, "DelayedStartup", {
           observers: ["chrome-document-loaded"],
           once: true,
           handler: _ => DelayedInit.scheduleList([
+            _ => Services.search.init(),
             _ => Services.logins,
           ], 10000 /* 10 seconds maximum wait. */),
         });
         break;
       }
 
-      case "chrome-document-global-created":
-      case "content-document-global-created": {
-        let win = GeckoViewUtils.getChromeWindow(subject);
-        if (win !== subject) {
+      case "chrome-document-interactive":
+      case "content-document-interactive": {
+        let contentWin = subject.QueryInterface(Ci.nsIDOMDocument).defaultView;
+        let win = GeckoViewUtils.getChromeWindow(contentWin);
+        let dispatcher = GeckoViewUtils.getDispatcherForWindow(win);
+        if (!win || !dispatcher || win !== contentWin) {
           // Only attach to top-level windows.
           return;
         }
 
         GeckoViewUtils.addLazyEventListener(win, "click", {
           handler: _ => [this.SelectHelper, this.InputWidgetHelper],
           options: {
             capture: true,
@@ -148,16 +156,33 @@ BrowserCLH.prototype = {
           },
           options: {
             capture: true,
             mozSystemGroup: true,
           },
         });
 
         this._initLoginManagerEvents(win);
+
+        GeckoViewUtils.registerLazyWindowEventListener(win, [
+          "TextSelection:Get",
+          "TextSelection:Action",
+          "TextSelection:End",
+        ], {
+          scope: this,
+          name: "ActionBarHandler",
+        });
+        GeckoViewUtils.addLazyEventListener(win, ["mozcaretstatechanged"], {
+          scope: this,
+          name: "ActionBarHandler",
+          options: {
+            capture: true,
+            mozSystemGroup: true,
+          },
+        });
         break;
       }
     }
   },
 
   _initLoginManagerEvents: function(aWindow) {
     if (Services.prefs.getBoolPref("reftest.remote", false)) {
       // XXX known incompatibility between reftest harness and form-fill.