diff options
Diffstat (limited to 'mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs')
-rw-r--r-- | mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs new file mode 100644 index 0000000000..f81c155678 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs @@ -0,0 +1,206 @@ +/* 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/. */ + +import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter"); + +export class GeckoViewPrompter { + constructor(aParent) { + this.id = Services.uuid.generateUUID().toString().slice(1, -1); // Discard surrounding braces + + if (aParent) { + if (Window.isInstance(aParent)) { + this._domWin = aParent; + } else if (aParent.window) { + this._domWin = aParent.window; + } else { + this._domWin = + aParent.embedderElement && aParent.embedderElement.ownerGlobal; + } + } + + if (!this._domWin) { + this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview"); + } + + this._innerWindowId = + this._domWin?.browsingContext.currentWindowContext.innerWindowId; + } + + get domWin() { + return this._domWin; + } + + get prompterActor() { + const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter"); + return actor; + } + + _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) { + console.error("Failed to change modal state:", ex); + } + return false; + } + + _dismissUi() { + this.prompterActor?.dismissPrompt(this); + } + + accept(aInputText = this.inputText) { + if (this.callback) { + let acceptMsg = {}; + switch (this.message.type) { + case "alert": + acceptMsg = null; + break; + case "button": + acceptMsg.button = 0; + break; + case "text": + acceptMsg.text = aInputText; + break; + default: + acceptMsg = null; + break; + } + this.callback(acceptMsg); + // Notify the UI that this prompt should be hidden. + this._dismissUi(); + } + } + + dismiss() { + this.callback(null); + // Notify the UI that this prompt should be hidden. + this._dismissUi(); + } + + getPromptType() { + switch (this.message.type) { + case "alert": + return this.message.checkValue ? "alertCheck" : "alert"; + case "button": + return this.message.checkValue ? "confirmCheck" : "confirm"; + case "text": + return this.message.checkValue ? "promptCheck" : "prompt"; + default: + return this.message.type; + } + } + + getPromptText() { + return this.message.msg; + } + + getInputText() { + return this.inputText; + } + + setInputText(aInput) { + this.inputText = aInput; + } + + /** + * 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( + "GeckoViewPrompter.jsm:showPrompt", + () => this._domWin.closed || result !== undefined + ); + } finally { + this._changeModalState(/* aEntering */ false); + } + return result; + } + + checkInnerWindow() { + // Checks that the innerWindow where this prompt was created still matches + // the current innerWindow. + // This checks will fail if the page navigates away, making this prompt + // obsolete. + return ( + this._innerWindowId === + this._domWin.browsingContext.currentWindowContext.innerWindowId + ); + } + + asyncShowPromptPromise(aMsg) { + return new Promise(resolve => { + this.asyncShowPrompt(aMsg, resolve); + }); + } + + async asyncShowPrompt(aMsg, aCallback) { + this.message = aMsg; + this.inputText = aMsg.value; + this.callback = aCallback; + + aMsg.id = this.id; + + let response = null; + try { + if (this.checkInnerWindow()) { + response = await this.prompterActor.prompt(this, aMsg); + } + } catch (error) { + // Nothing we can do really, we will treat this as a dismiss. + warn`Error while prompting: ${error}`; + } + + if (!this.checkInnerWindow()) { + // Page has navigated away, let's dismiss the prompt + aCallback(null); + } else { + 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; + } + + update(aMsg) { + this.message = aMsg; + aMsg.id = this.id; + this.prompterActor?.updatePrompt(aMsg); + } +} |