Bug 1629113 - Factor out prompter logic in GeckoViewPrompter. r=droeh
authorAgi Sferro <agi@sferro.dev>
Fri, 22 May 2020 23:22:34 +0000
changeset 531743 54af712fe5983704e25b581afbd760557f24e948
parent 531742 b4aa2007863153e3b39aef004689964d728b1038
child 531744 1f2dfbcafe57e8aac38dca4df96e691b659bae8e
push id37442
push userncsoregi@mozilla.com
push dateSat, 23 May 2020 09:21:24 +0000
treeherdermozilla-central@bbcc193fe0f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdroeh
bugs1629113
milestone78.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 1629113 - Factor out prompter logic in GeckoViewPrompter. r=droeh Differential Revision: https://phabricator.services.mozilla.com/D75874
mobile/android/components/geckoview/GeckoViewPrompt.js
mobile/android/components/geckoview/GeckoViewPrompter.jsm
mobile/android/components/geckoview/moz.build
--- a/mobile/android/components/geckoview/GeckoViewPrompt.js
+++ b/mobile/android/components/geckoview/GeckoViewPrompt.js
@@ -1,42 +1,46 @@
 /* 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/. */
 
+const { GeckoViewUtils } = ChromeUtils.import(
+  "resource://gre/modules/GeckoViewUtils.jsm"
+);
+
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   EventDispatcher: "resource://gre/modules/Messaging.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
-  GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
   GeckoViewLoginStorage: "resource://gre/modules/GeckoViewLoginStorage.jsm",
   LoginEntry: "resource://gre/modules/GeckoViewLoginStorage.jsm",
+  GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "UUIDGen",
   "@mozilla.org/uuid-generator;1",
   "nsIUUIDGenerator"
 );
 
 const domBundle = Services.strings.createBundle(
   "chrome://global/locale/dom/dom.properties"
 );
 
+const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt"); // eslint-disable-line no-unused-vars
+
 function PromptFactory() {
   this.wrappedJSObject = this;
 }
 
-const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt"); // eslint-disable-line no-unused-vars
-
 PromptFactory.prototype = {
   classID: Components.ID("{076ac188-23c1-4390-aa08-7ef1f78ca5d9}"),
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIPromptFactory,
     Ci.nsIPromptService,
   ]),
 
@@ -113,17 +117,17 @@ PromptFactory.prototype = {
           continue;
         }
         items.push(item);
         map[id++] = child;
       }
       return items;
     })(aElement);
 
-    const prompt = new PromptDelegate(win);
+    const prompt = new GeckoViewPrompter(win);
     prompt.asyncShowPrompt(
       {
         type: "choice",
         mode: aElement.multiple ? "multiple" : "single",
         choices: items,
       },
       result => {
         // OK: result
@@ -170,17 +174,17 @@ PromptFactory.prototype = {
         if (dispatchEvents) {
           this._dispatchEvents(aElement);
         }
       }
     );
   },
 
   _handleDateTime(aElement, aType) {
-    const prompt = new PromptDelegate(aElement.ownerGlobal);
+    const prompt = new GeckoViewPrompter(aElement.ownerGlobal);
     prompt.asyncShowPrompt(
       {
         type: "datetime",
         mode: aType,
         value: aElement.value,
         min: aElement.min,
         max: aElement.max,
       },
@@ -308,17 +312,17 @@ PromptFactory.prototype = {
         }
       },
     };
 
     // XXX the "show" event is not cancelable but spec says it should be.
     menu.sendShowEvent();
     menu.build(builder);
 
-    const prompt = new PromptDelegate(target.ownerGlobal);
+    const prompt = new GeckoViewPrompter(target.ownerGlobal);
     prompt.asyncShowPrompt(
       {
         type: "choice",
         mode: "menu",
         choices: builder.items,
       },
       result => {
         // OK: result
@@ -333,17 +337,17 @@ PromptFactory.prototype = {
   },
 
   _handlePopupBlocked(aEvent) {
     const dwi = aEvent.requestingWindow;
     const popupWindowURISpec = aEvent.popupWindowURI
       ? aEvent.popupWindowURI.displaySpec
       : "about:blank";
 
-    const prompt = new PromptDelegate(aEvent.requestingWindow);
+    const prompt = new GeckoViewPrompter(aEvent.requestingWindow);
     prompt.asyncShowPrompt(
       {
         type: "popup",
         targetUri: popupWindowURISpec,
       },
       ({ response }) => {
         if (response && dwi) {
           dwi.open(
@@ -457,131 +461,28 @@ PromptFactory.prototype = {
     return this.callProxy("asyncPromptAuth", arguments);
   },
   asyncPromptAuthBC() {
     return this.callProxy("asyncPromptAuth", arguments);
   },
 };
 
 function PromptDelegate(aParent) {
-  if (aParent) {
-    if (aParent instanceof Window) {
-      this._domWin = aParent;
-    } else if (aParent.window) {
-      this._domWin = aParent.window;
-    } else {
-      this._domWin =
-        aParent.embedderElement && aParent.embedderElement.ownerGlobal;
-    }
-  }
-
-  if (this._domWin) {
-    this._dispatcher = GeckoViewUtils.getDispatcherForWindow(this._domWin);
-  }
-
-  if (!this._dispatcher) {
-    [
-      this._dispatcher,
-      this._domWin,
-    ] = GeckoViewUtils.getActiveDispatcherAndWindow();
-  }
+  this._prompter = new GeckoViewPrompter(aParent);
 }
 
 PromptDelegate.prototype = {
   QueryInterface: ChromeUtils.generateQI([Ci.nsIPrompt]),
 
   BUTTON_TYPE_POSITIVE: 0,
   BUTTON_TYPE_NEUTRAL: 1,
   BUTTON_TYPE_NEGATIVE: 2,
 
   /* ---------- internal methods ---------- */
 
-  _changeModalState(aEntering) {
-    if (!this._domWin) {
-      // Allow not having a DOM window.
-      return true;
-    }
-    // Accessing the document object can throw if this window no longer exists. See bug 789888.
-    try {
-      const winUtils = this._domWin.windowUtils;
-      if (!aEntering) {
-        winUtils.leaveModalState();
-      }
-
-      const event = this._domWin.document.createEvent("Events");
-      event.initEvent(
-        aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed",
-        true,
-        true
-      );
-      winUtils.dispatchEventToChromeOnly(this._domWin, event);
-
-      if (aEntering) {
-        winUtils.enterModalState();
-      }
-      return true;
-    } catch (ex) {
-      Cu.reportError("Failed to change modal state: " + ex);
-    }
-    return false;
-  },
-
-  /**
-   * Shows a native prompt, and then spins the event loop for this thread while we wait
-   * for a response
-   */
-  _showPrompt(aMsg) {
-    let result = undefined;
-    if (!this._domWin || !this._changeModalState(/* aEntering */ true)) {
-      return result;
-    }
-    try {
-      this.asyncShowPrompt(aMsg, res => (result = res));
-
-      // Spin this thread while we wait for a result
-      Services.tm.spinEventLoopUntil(
-        () => this._domWin.closed || result !== undefined
-      );
-    } finally {
-      this._changeModalState(/* aEntering */ false);
-    }
-    return result;
-  },
-
-  asyncShowPrompt(aMsg, aCallback) {
-    let handled = false;
-    const onResponse = response => {
-      if (handled) {
-        return;
-      }
-      aCallback(response);
-      // This callback object is tied to the Java garbage collector because
-      // it is invoked from Java. Manually release the target callback
-      // here; otherwise we may hold onto resources for too long, because
-      // we would be relying on both the Java and the JS garbage collectors
-      // to run.
-      aMsg = undefined;
-      aCallback = undefined;
-      handled = true;
-    };
-
-    if (!this._dispatcher) {
-      onResponse(null);
-      return;
-    }
-
-    this._dispatcher.dispatch("GeckoView:Prompt", aMsg, {
-      onSuccess: onResponse,
-      onError: error => {
-        Cu.reportError("Prompt error: " + error);
-        onResponse(null);
-      },
-    });
-  },
-
   _addText(aTitle, aText, aMsg) {
     return Object.assign(aMsg, {
       title: aTitle,
       msg: aText,
     });
   },
 
   _addCheck(aCheckMsg, aCheckState, aMsg) {
@@ -594,17 +495,17 @@ PromptDelegate.prototype = {
 
   /* ----------  nsIPrompt  ---------- */
 
   alert(aTitle, aText) {
     this.alertCheck(aTitle, aText);
   },
 
   alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
-    const result = this._showPrompt(
+    const result = this._prompter.showPrompt(
       this._addText(
         aTitle,
         aText,
         this._addCheck(aCheckMsg, aCheckState, {
           type: "alert",
         })
       )
     );
@@ -684,17 +585,17 @@ PromptDelegate.prototype = {
     for (let i = 0; i < 3 && savedButtonId.length; i++) {
       if (btnMap[i] === null) {
         btnMap[i] = savedButtonId.shift();
         btnTitle[i] = "custom";
         btnCustomTitle[i] = [aButton0, aButton1, aButton2][btnMap[i]];
       }
     }
 
-    const result = this._showPrompt(
+    const result = this._prompter.showPrompt(
       this._addText(
         aTitle,
         aText,
         this._addCheck(aCheckMsg, aCheckState, {
           type: "button",
           btnTitle,
           btnCustomTitle,
         })
@@ -702,17 +603,17 @@ PromptDelegate.prototype = {
     );
     if (result && aCheckState) {
       aCheckState.value = !!result.checkValue;
     }
     return result && result.button in btnMap ? btnMap[result.button] : -1;
   },
 
   prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
-    const result = this._showPrompt(
+    const result = this._prompter.showPrompt(
       this._addText(
         aTitle,
         aText,
         this._addCheck(aCheckMsg, aCheckState, {
           type: "text",
           value: aValue.value,
         })
       )
@@ -753,17 +654,17 @@ PromptDelegate.prototype = {
       type: "auth",
       mode: aUsername ? "auth" : "password",
       options: {
         flags: aUsername ? 0 : Ci.nsIAuthInformation.ONLY_PASSWORD,
         username: aUsername ? aUsername.value : undefined,
         password: aPassword.value,
       },
     };
-    const result = this._showPrompt(
+    const result = this._prompter.showPrompt(
       this._addText(aTitle, aText, this._addCheck(aCheckMsg, aCheckState, msg))
     );
     // OK: result && result.password !== undefined
     // Cancel: result && result.password === undefined
     // Error: !result
     if (result && aCheckState) {
       aCheckState.value = !!result.checkValue;
     }
@@ -779,17 +680,17 @@ PromptDelegate.prototype = {
 
   select(aTitle, aText, aSelectList, aOutSelection) {
     const choices = Array.prototype.map.call(aSelectList, (item, index) => ({
       id: String(index),
       label: item,
       disabled: false,
       selected: false,
     }));
-    const result = this._showPrompt(
+    const result = this._prompter.showPrompt(
       this._addText(aTitle, aText, {
         type: "choice",
         mode: "single",
         choices,
       })
     );
     // OK: result
     // Cancel: !result
@@ -853,17 +754,17 @@ PromptDelegate.prototype = {
         return true;
       }
     }
     aAuthInfo.username = username;
     return true;
   },
 
   promptAuth(aChannel, aLevel, aAuthInfo, aCheckMsg, aCheckState) {
-    const result = this._showPrompt(
+    const result = this._prompter.showPrompt(
       this._addCheck(
         aCheckMsg,
         aCheckState,
         this._getAuthMsg(aChannel, aLevel, aAuthInfo)
       )
     );
     // OK: result && result.password !== undefined
     // Cancel: result && result.password === undefined
@@ -890,17 +791,17 @@ PromptDelegate.prototype = {
       }
       responded = true;
       if (this._fillAuthInfo(aAuthInfo, aCheckState, result)) {
         aCallback.onAuthAvailable(aContext, aAuthInfo);
       } else {
         aCallback.onAuthCancelled(aContext, /* userCancel */ true);
       }
     };
-    this.asyncShowPrompt(
+    this._prompter.asyncShowPrompt(
       this._addCheck(
         aCheckMsg,
         aCheckState,
         this._getAuthMsg(aChannel, aLevel, aAuthInfo)
       ),
       callback
     );
     return {
@@ -1019,17 +920,17 @@ FilePickerDelegate.prototype = {
   /* ----------  nsIFilePicker  ---------- */
   init(aParent, aTitle, aMode) {
     if (
       aMode === Ci.nsIFilePicker.modeGetFolder ||
       aMode === Ci.nsIFilePicker.modeSave
     ) {
       throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
     }
-    this._prompt = new PromptDelegate(aParent);
+    this._prompt = new GeckoViewPrompter(aParent);
     this._msg = {
       type: "file",
       title: aTitle,
       mode: aMode === Ci.nsIFilePicker.modeOpenMultiple ? "multiple" : "single",
     };
     this._mode = aMode;
     this._mimeTypes = [];
     this._capture = 0;
@@ -1110,18 +1011,18 @@ FilePickerDelegate.prototype = {
     }
   },
 
   get files() {
     return this._getEnumerator(/* aDOMFile */ false);
   },
 
   _getDOMFile(aPath) {
-    if (this._prompt._domWin) {
-      return this._prompt._domWin.File.createFromFileName(aPath);
+    if (this._prompt.domWin) {
+      return this._prompt.domWin.File.createFromFileName(aPath);
     }
     return File.createFromFileName(aPath);
   },
 
   get domFileOrDirectory() {
     if (!this._fileData) {
       throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
     }
@@ -1186,17 +1087,17 @@ FilePickerDelegate.prototype = {
 function ColorPickerDelegate() {}
 
 ColorPickerDelegate.prototype = {
   classID: Components.ID("{aa0dd6fc-73dd-4621-8385-c0b377e02cee}"),
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIColorPicker]),
 
   init(aParent, aTitle, aInitialColor) {
-    this._prompt = new PromptDelegate(aParent);
+    this._prompt = new GeckoViewPrompter(aParent);
     this._msg = {
       type: "color",
       title: aTitle,
       value: aInitialColor,
     };
   },
 
   open(aColorPickerShownCallback) {
@@ -1229,23 +1130,23 @@ ShareDelegate.prototype = {
     const SUCCESS = 0;
 
     const msg = {
       type: "share",
       title: aTitle,
       text: aText,
       uri: aUri ? aUri.displaySpec : null,
     };
-    const prompt = new PromptDelegate(this._openerWindow);
+    const prompt = new GeckoViewPrompter(this._openerWindow);
     const result = await new Promise(resolve => {
       prompt.asyncShowPrompt(msg, resolve);
     });
 
     if (!result) {
-      // A null result is treated as a dismissal in PromptDelegate.
+      // A null result is treated as a dismissal in GeckoViewPrompter.
       throw new DOMException(
         domBundle.GetStringFromName("WebShareAPI_Aborted"),
         "AbortError"
       );
     }
 
     const res = result && result.response;
     switch (res) {
@@ -1292,17 +1193,17 @@ class LoginStorageDelegate {
   }
 
   promptToSavePassword(
     aBrowser,
     aLogin,
     dismissed = false,
     notifySaved = false
   ) {
-    const prompt = new PromptDelegate(aBrowser.ownerGlobal);
+    const prompt = new GeckoViewPrompter(aBrowser.ownerGlobal);
     prompt.asyncShowPrompt(
       this._createMessage(LoginStorageType.SAVE, LoginStorageHint.NONE, [
         LoginEntry.fromLoginInfo(aLogin),
       ]),
       result => {
         if (!result || result.login === undefined) {
           return;
         }
@@ -1325,17 +1226,17 @@ class LoginStorageDelegate {
   ) {
     const newLogin = LoginEntry.fromLoginInfo(aOldLogin || aNewLogin);
     const oldGuid = (aOldLogin && newLogin.guid) || null;
     newLogin.origin = aNewLogin.origin;
     newLogin.formActionOrigin = aNewLogin.formActionOrigin;
     newLogin.password = aNewLogin.password;
     newLogin.username = aNewLogin.username;
 
-    const prompt = new PromptDelegate(aBrowser.ownerGlobal);
+    const prompt = new GeckoViewPrompter(aBrowser.ownerGlobal);
     prompt.asyncShowPrompt(
       this._createMessage(LoginStorageType.SAVE, LoginStorageHint.NONE, [
         newLogin,
       ]),
       result => {
         if (!result || result.login === undefined) {
           return;
         }
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewPrompter.jsm
@@ -0,0 +1,127 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["GeckoViewPrompter"];
+
+const { GeckoViewUtils } = ChromeUtils.import(
+  "resource://gre/modules/GeckoViewUtils.jsm"
+);
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter"); // eslint-disable-line no-unused-vars
+
+class GeckoViewPrompter {
+  constructor(aParent) {
+    if (aParent) {
+      if (aParent instanceof Window) {
+        this._domWin = aParent;
+      } else if (aParent.window) {
+        this._domWin = aParent.window;
+      } else {
+        this._domWin =
+          aParent.embedderElement && aParent.embedderElement.ownerGlobal;
+      }
+    }
+
+    if (this._domWin) {
+      this._dispatcher = GeckoViewUtils.getDispatcherForWindow(this._domWin);
+    }
+
+    if (!this._dispatcher) {
+      [
+        this._dispatcher,
+        this._domWin,
+      ] = GeckoViewUtils.getActiveDispatcherAndWindow();
+    }
+  }
+
+  get domWin() {
+    return this._domWin;
+  }
+
+  _changeModalState(aEntering) {
+    if (!this._domWin) {
+      // Allow not having a DOM window.
+      return true;
+    }
+    // Accessing the document object can throw if this window no longer exists. See bug 789888.
+    try {
+      const winUtils = this._domWin.windowUtils;
+      if (!aEntering) {
+        winUtils.leaveModalState();
+      }
+
+      const event = this._domWin.document.createEvent("Events");
+      event.initEvent(
+        aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed",
+        true,
+        true
+      );
+      winUtils.dispatchEventToChromeOnly(this._domWin, event);
+
+      if (aEntering) {
+        winUtils.enterModalState();
+      }
+      return true;
+    } catch (ex) {
+      Cu.reportError("Failed to change modal state: " + ex);
+    }
+    return false;
+  }
+
+  /**
+   * Shows a native prompt, and then spins the event loop for this thread while we wait
+   * for a response
+   */
+  showPrompt(aMsg) {
+    let result = undefined;
+    if (!this._domWin || !this._changeModalState(/* aEntering */ true)) {
+      return result;
+    }
+    try {
+      this.asyncShowPrompt(aMsg, res => (result = res));
+
+      // Spin this thread while we wait for a result
+      Services.tm.spinEventLoopUntil(
+        () => this._domWin.closed || result !== undefined
+      );
+    } finally {
+      this._changeModalState(/* aEntering */ false);
+    }
+    return result;
+  }
+
+  asyncShowPrompt(aMsg, aCallback) {
+    let handled = false;
+    const onResponse = response => {
+      if (handled) {
+        return;
+      }
+      aCallback(response);
+      // This callback object is tied to the Java garbage collector because
+      // it is invoked from Java. Manually release the target callback
+      // here; otherwise we may hold onto resources for too long, because
+      // we would be relying on both the Java and the JS garbage collectors
+      // to run.
+      aMsg = undefined;
+      aCallback = undefined;
+      handled = true;
+    };
+
+    if (!this._dispatcher) {
+      onResponse(null);
+      return;
+    }
+
+    this._dispatcher.dispatch("GeckoView:Prompt", aMsg, {
+      onSuccess: onResponse,
+      onError: error => {
+        Cu.reportError("Prompt error: " + error);
+        onResponse(null);
+      },
+    });
+  }
+}
--- a/mobile/android/components/geckoview/moz.build
+++ b/mobile/android/components/geckoview/moz.build
@@ -22,9 +22,13 @@ if CONFIG['MOZ_ANDROID_HISTORY']:
 EXTRA_COMPONENTS += [
     'GeckoView.manifest',
     'GeckoViewPermission.js',
     'GeckoViewPrompt.js',
     'GeckoViewPush.js',
     'GeckoViewStartup.js',
 ]
 
+EXTRA_JS_MODULES += [
+    'GeckoViewPrompter.jsm',
+]
+
 FINAL_LIBRARY = 'xul'