summaryrefslogtreecommitdiffstats
path: root/mobile/android/components/geckoview
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/components/geckoview')
-rw-r--r--mobile/android/components/geckoview/ColorPickerDelegate.jsm43
-rw-r--r--mobile/android/components/geckoview/FilePickerDelegate.jsm194
-rw-r--r--mobile/android/components/geckoview/GeckoView.manifest4
-rw-r--r--mobile/android/components/geckoview/GeckoViewExternalAppService.cpp100
-rw-r--r--mobile/android/components/geckoview/GeckoViewExternalAppService.h26
-rw-r--r--mobile/android/components/geckoview/GeckoViewHistory.cpp508
-rw-r--r--mobile/android/components/geckoview/GeckoViewHistory.h60
-rw-r--r--mobile/android/components/geckoview/GeckoViewOutputStream.cpp61
-rw-r--r--mobile/android/components/geckoview/GeckoViewOutputStream.h29
-rw-r--r--mobile/android/components/geckoview/GeckoViewPermission.jsm41
-rw-r--r--mobile/android/components/geckoview/GeckoViewPrompt.jsm826
-rw-r--r--mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs206
-rw-r--r--mobile/android/components/geckoview/GeckoViewPush.jsm257
-rw-r--r--mobile/android/components/geckoview/GeckoViewStartup.jsm344
-rw-r--r--mobile/android/components/geckoview/GeckoViewStreamListener.cpp298
-rw-r--r--mobile/android/components/geckoview/GeckoViewStreamListener.h57
-rw-r--r--mobile/android/components/geckoview/LoginStorageDelegate.jsm138
-rw-r--r--mobile/android/components/geckoview/PromptCollection.jsm48
-rw-r--r--mobile/android/components/geckoview/ShareDelegate.jsm82
-rw-r--r--mobile/android/components/geckoview/components.conf93
-rw-r--r--mobile/android/components/geckoview/moz.build49
21 files changed, 3464 insertions, 0 deletions
diff --git a/mobile/android/components/geckoview/ColorPickerDelegate.jsm b/mobile/android/components/geckoview/ColorPickerDelegate.jsm
new file mode 100644
index 0000000000..b72662a316
--- /dev/null
+++ b/mobile/android/components/geckoview/ColorPickerDelegate.jsm
@@ -0,0 +1,43 @@
+/* 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 = ["ColorPickerDelegate"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
+});
+
+const { debug, warn } = GeckoViewUtils.initLogging("ColorPickerDelegate");
+
+class ColorPickerDelegate {
+ // TODO(bug 1805397): Implement default colors
+ init(aParent, aTitle, aInitialColor, aDefaultColors) {
+ this._prompt = new lazy.GeckoViewPrompter(aParent);
+ this._msg = {
+ type: "color",
+ title: aTitle,
+ value: aInitialColor,
+ predefinedValues: aDefaultColors,
+ };
+ }
+
+ open(aColorPickerShownCallback) {
+ this._prompt.asyncShowPrompt(this._msg, result => {
+ // OK: result
+ // Cancel: !result
+ aColorPickerShownCallback.done((result && result.color) || "");
+ });
+ }
+}
+
+ColorPickerDelegate.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIColorPicker",
+]);
diff --git a/mobile/android/components/geckoview/FilePickerDelegate.jsm b/mobile/android/components/geckoview/FilePickerDelegate.jsm
new file mode 100644
index 0000000000..32966b65e6
--- /dev/null
+++ b/mobile/android/components/geckoview/FilePickerDelegate.jsm
@@ -0,0 +1,194 @@
+/* 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 = ["FilePickerDelegate"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
+});
+
+const { debug, warn } = GeckoViewUtils.initLogging("FilePickerDelegate");
+
+class FilePickerDelegate {
+ /* ---------- 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 lazy.GeckoViewPrompter(aParent);
+ this._msg = {
+ type: "file",
+ title: aTitle,
+ mode: aMode === Ci.nsIFilePicker.modeOpenMultiple ? "multiple" : "single",
+ };
+ this._mode = aMode;
+ this._mimeTypes = [];
+ this._capture = 0;
+ }
+
+ get mode() {
+ return this._mode;
+ }
+
+ appendRawFilter(aFilter) {
+ this._mimeTypes.push(aFilter);
+ }
+
+ show() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ open(aFilePickerShownCallback) {
+ this._msg.mimeTypes = this._mimeTypes;
+ this._msg.capture = this._capture;
+ this._prompt.asyncShowPrompt(this._msg, result => {
+ // OK: result
+ // Cancel: !result
+ if (!result || !result.files || !result.files.length) {
+ aFilePickerShownCallback.done(Ci.nsIFilePicker.returnCancel);
+ } else {
+ this._resolveFiles(result.files, aFilePickerShownCallback);
+ }
+ });
+ }
+
+ async _resolveFiles(aFiles, aCallback) {
+ const fileData = [];
+
+ try {
+ for (const file of aFiles) {
+ const domFile = await this._getDOMFile(file);
+ fileData.push({
+ file,
+ domFile,
+ });
+ }
+ } catch (ex) {
+ warn`Error resolving files from file picker: ${ex}`;
+ aCallback.done(Ci.nsIFilePicker.returnCancel);
+ return;
+ }
+
+ this._fileData = fileData;
+ aCallback.done(Ci.nsIFilePicker.returnOK);
+ }
+
+ get file() {
+ if (!this._fileData) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+ const fileData = this._fileData[0];
+ if (!fileData) {
+ return null;
+ }
+ return new lazy.FileUtils.File(fileData.file);
+ }
+
+ get fileURL() {
+ return Services.io.newFileURI(this.file);
+ }
+
+ *_getEnumerator(aDOMFile) {
+ if (!this._fileData) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+ }
+
+ for (const fileData of this._fileData) {
+ if (aDOMFile) {
+ yield fileData.domFile;
+ }
+ yield new lazy.FileUtils.File(fileData.file);
+ }
+ }
+
+ get files() {
+ return this._getEnumerator(/* aDOMFile */ false);
+ }
+
+ _getDOMFile(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);
+ }
+ return this._fileData[0] ? this._fileData[0].domFile : null;
+ }
+
+ get domFileOrDirectoryEnumerator() {
+ return this._getEnumerator(/* aDOMFile */ true);
+ }
+
+ get defaultString() {
+ return "";
+ }
+
+ set defaultString(aValue) {}
+
+ get defaultExtension() {
+ return "";
+ }
+
+ set defaultExtension(aValue) {}
+
+ get filterIndex() {
+ return 0;
+ }
+
+ set filterIndex(aValue) {}
+
+ get displayDirectory() {
+ return null;
+ }
+
+ set displayDirectory(aValue) {}
+
+ get displaySpecialDirectory() {
+ return "";
+ }
+
+ set displaySpecialDirectory(aValue) {}
+
+ get addToRecentDocs() {
+ return false;
+ }
+
+ set addToRecentDocs(aValue) {}
+
+ get okButtonLabel() {
+ return "";
+ }
+
+ set okButtonLabel(aValue) {}
+
+ get capture() {
+ return this._capture;
+ }
+
+ set capture(aValue) {
+ this._capture = aValue;
+ }
+}
+
+FilePickerDelegate.prototype.classID = Components.ID(
+ "{e4565e36-f101-4bf5-950b-4be0887785a9}"
+);
+FilePickerDelegate.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIFilePicker",
+]);
diff --git a/mobile/android/components/geckoview/GeckoView.manifest b/mobile/android/components/geckoview/GeckoView.manifest
new file mode 100644
index 0000000000..472c4d7298
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoView.manifest
@@ -0,0 +1,4 @@
+# GeckoViewStartup.js
+category app-startup GeckoViewStartup @mozilla.org/geckoview/startup;1 process=main
+category content-process-ready-for-script GeckoViewStartup @mozilla.org/geckoview/startup;1 process=content
+category profile-after-change GeckoViewStartup @mozilla.org/geckoview/startup;1 process=main
diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp
new file mode 100644
index 0000000000..30dedc9251
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+#include "GeckoViewExternalAppService.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "nsIChannel.h"
+
+#include "mozilla/widget/EventDispatcher.h"
+#include "mozilla/widget/nsWindow.h"
+#include "GeckoViewStreamListener.h"
+
+#include "JavaBuiltins.h"
+
+class StreamListener final : public mozilla::GeckoViewStreamListener {
+ public:
+ explicit StreamListener(nsWindow* aWindow)
+ : GeckoViewStreamListener(), mWindow(aWindow) {}
+
+ void SendWebResponse(mozilla::java::WebResponse::Param aResponse) {
+ mWindow->PassExternalResponse(aResponse);
+ }
+
+ void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) {
+ // Currently we don't do anything about errors here
+ }
+
+ virtual ~StreamListener() {}
+
+ private:
+ RefPtr<nsWindow> mWindow;
+};
+
+mozilla::StaticRefPtr<GeckoViewExternalAppService>
+ GeckoViewExternalAppService::sService;
+
+/* static */
+already_AddRefed<GeckoViewExternalAppService>
+GeckoViewExternalAppService::GetSingleton() {
+ if (!sService) {
+ sService = new GeckoViewExternalAppService();
+ }
+ RefPtr<GeckoViewExternalAppService> service = sService;
+ return service.forget();
+}
+
+GeckoViewExternalAppService::GeckoViewExternalAppService() {}
+
+NS_IMPL_ISUPPORTS(GeckoViewExternalAppService, nsIExternalHelperAppService);
+
+NS_IMETHODIMP GeckoViewExternalAppService::DoContent(
+ const nsACString& aMimeContentType, nsIRequest* aRequest,
+ nsIInterfaceRequestor* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP GeckoViewExternalAppService::CreateListener(
+ const nsACString& aMimeContentType, nsIRequest* aRequest,
+ mozilla::dom::BrowsingContext* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ using namespace mozilla;
+ using namespace mozilla::dom;
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsresult rv;
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIWidget> widget =
+ aContentContext->Canonical()->GetParentProcessWidgetContaining();
+ if (!widget) {
+ return NS_ERROR_ABORT;
+ }
+
+ RefPtr<nsWindow> window = nsWindow::From(widget);
+ MOZ_ASSERT(window);
+
+ RefPtr<StreamListener> listener = new StreamListener(window);
+
+ rv = channel->SetNotificationCallbacks(listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ listener.forget(aStreamListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP GeckoViewExternalAppService::ApplyDecodingForExtension(
+ const nsACString& aExtension, const nsACString& aEncodingType,
+ bool* aApplyDecoding) {
+ // This currently doesn't matter, because we never read the stream.
+ *aApplyDecoding = true;
+ return NS_OK;
+}
diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.h b/mobile/android/components/geckoview/GeckoViewExternalAppService.h
new file mode 100644
index 0000000000..1dfb7c9491
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+#ifndef GeckoViewExternalAppService_h__
+#define GeckoViewExternalAppService_h__
+
+#include "nsIExternalHelperAppService.h"
+#include "mozilla/StaticPtr.h"
+
+class GeckoViewExternalAppService : public nsIExternalHelperAppService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIEXTERNALHELPERAPPSERVICE
+
+ GeckoViewExternalAppService();
+
+ static already_AddRefed<GeckoViewExternalAppService> GetSingleton();
+
+ private:
+ virtual ~GeckoViewExternalAppService() {}
+ static mozilla::StaticRefPtr<GeckoViewExternalAppService> sService;
+};
+
+#endif
diff --git a/mobile/android/components/geckoview/GeckoViewHistory.cpp b/mobile/android/components/geckoview/GeckoViewHistory.cpp
new file mode 100644
index 0000000000..c69e419b18
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewHistory.cpp
@@ -0,0 +1,508 @@
+/* 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/. */
+
+#include "GeckoViewHistory.h"
+
+#include "JavaBuiltins.h"
+#include "jsapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "nsIURI.h"
+#include "nsXULAppAPI.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/StaticPrefs_layout.h"
+
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Link.h"
+#include "mozilla/dom/BrowserChild.h"
+
+#include "mozilla/ipc/URIUtils.h"
+
+#include "mozilla/widget/EventDispatcher.h"
+#include "mozilla/widget/nsWindow.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+using namespace mozilla::widget;
+
+static const char16_t kOnVisitedMessage[] = u"GeckoView:OnVisited";
+static const char16_t kGetVisitedMessage[] = u"GeckoView:GetVisited";
+
+// Keep in sync with `GeckoSession.HistoryDelegate.VisitFlags`.
+enum class GeckoViewVisitFlags : int32_t {
+ VISIT_TOP_LEVEL = 1 << 0,
+ VISIT_REDIRECT_TEMPORARY = 1 << 1,
+ VISIT_REDIRECT_PERMANENT = 1 << 2,
+ VISIT_REDIRECT_SOURCE = 1 << 3,
+ VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4,
+ VISIT_UNRECOVERABLE_ERROR = 1 << 5,
+};
+
+GeckoViewHistory::GeckoViewHistory() {}
+
+GeckoViewHistory::~GeckoViewHistory() {}
+
+NS_IMPL_ISUPPORTS(GeckoViewHistory, IHistory)
+
+StaticRefPtr<GeckoViewHistory> GeckoViewHistory::sHistory;
+
+/* static */
+already_AddRefed<GeckoViewHistory> GeckoViewHistory::GetSingleton() {
+ if (!sHistory) {
+ sHistory = new GeckoViewHistory();
+ ClearOnShutdown(&sHistory);
+ }
+ RefPtr<GeckoViewHistory> history = sHistory;
+ return history.forget();
+}
+
+// Handles a request to fetch visited statuses for new tracked URIs in the
+// content process (e10s).
+void GeckoViewHistory::QueryVisitedStateInContentProcess(
+ const PendingVisitedQueries& aQueries) {
+ // Holds an array of new tracked URIs for a tab in the content process.
+ struct NewURIEntry {
+ explicit NewURIEntry(BrowserChild* aBrowserChild, nsIURI* aURI)
+ : mBrowserChild(aBrowserChild) {
+ AddURI(aURI);
+ }
+
+ void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); }
+
+ BrowserChild* mBrowserChild;
+ nsTArray<RefPtr<nsIURI>> mURIs;
+ };
+
+ MOZ_ASSERT(XRE_IsContentProcess());
+
+ // First, serialize all the new URIs that we need to look up. Note that this
+ // could be written as `nsTHashMap<nsUint64HashKey,
+ // nsTArray<URIParams>` instead, but, since we don't expect to have many tab
+ // children, we can avoid the cost of hashing.
+ AutoTArray<NewURIEntry, 8> newEntries;
+ for (auto& query : aQueries) {
+ nsIURI* uri = query.GetKey();
+ MOZ_ASSERT(query.GetData().IsEmpty(),
+ "Shouldn't have parents to notify in child processes");
+ auto entry = mTrackedURIs.Lookup(uri);
+ if (!entry) {
+ continue;
+ }
+ ObservingLinks& links = entry.Data();
+ for (Link* link : links.mLinks.BackwardRange()) {
+ nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement());
+ if (!widget) {
+ continue;
+ }
+ BrowserChild* browserChild = widget->GetOwningBrowserChild();
+ if (!browserChild) {
+ continue;
+ }
+ // Add to the list of new URIs for this document, or make a new entry.
+ bool hasEntry = false;
+ for (NewURIEntry& entry : newEntries) {
+ if (entry.mBrowserChild == browserChild) {
+ entry.AddURI(uri);
+ hasEntry = true;
+ break;
+ }
+ }
+ if (!hasEntry) {
+ newEntries.AppendElement(NewURIEntry(browserChild, uri));
+ }
+ }
+ }
+
+ // Send the request to the parent process, one message per tab child.
+ for (const NewURIEntry& entry : newEntries) {
+ Unused << NS_WARN_IF(
+ !entry.mBrowserChild->SendQueryVisitedState(entry.mURIs));
+ }
+}
+
+// Handles a request to fetch visited statuses for new tracked URIs in the
+// parent process (non-e10s).
+void GeckoViewHistory::QueryVisitedStateInParentProcess(
+ const PendingVisitedQueries& aQueries) {
+ // Holds an array of new URIs for a window in the parent process. Unlike
+ // the content process case, we don't need to track tab children, since we
+ // have the outer window and can send the request directly to Java.
+ struct NewURIEntry {
+ explicit NewURIEntry(nsIWidget* aWidget, nsIURI* aURI) : mWidget(aWidget) {
+ AddURI(aURI);
+ }
+
+ void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); }
+
+ nsCOMPtr<nsIWidget> mWidget;
+ nsTArray<RefPtr<nsIURI>> mURIs;
+ };
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsTArray<NewURIEntry> newEntries;
+ for (const auto& query : aQueries) {
+ nsIURI* uri = query.GetKey();
+ auto entry = mTrackedURIs.Lookup(uri);
+ if (!entry) {
+ continue; // Nobody cares about this uri anymore.
+ }
+
+ ObservingLinks& links = entry.Data();
+ nsTObserverArray<Link*>::BackwardIterator linksIter(links.mLinks);
+ while (linksIter.HasMore()) {
+ Link* link = linksIter.GetNext();
+
+ nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement());
+ if (!widget) {
+ continue;
+ }
+
+ bool hasEntry = false;
+ for (NewURIEntry& entry : newEntries) {
+ if (entry.mWidget != widget) {
+ continue;
+ }
+ entry.AddURI(uri);
+ hasEntry = true;
+ }
+ if (!hasEntry) {
+ newEntries.AppendElement(NewURIEntry(widget, uri));
+ }
+ }
+ }
+
+ for (NewURIEntry& entry : newEntries) {
+ QueryVisitedState(entry.mWidget, nullptr, std::move(entry.mURIs));
+ }
+}
+
+void GeckoViewHistory::StartPendingVisitedQueries(
+ PendingVisitedQueries&& aQueries) {
+ if (XRE_IsContentProcess()) {
+ QueryVisitedStateInContentProcess(aQueries);
+ } else {
+ QueryVisitedStateInParentProcess(aQueries);
+ }
+}
+
+/**
+ * Called from the session handler for the history delegate, after the new
+ * visit is recorded.
+ */
+class OnVisitedCallback final : public nsIAndroidEventCallback {
+ public:
+ explicit OnVisitedCallback(GeckoViewHistory* aHistory, nsIURI* aURI)
+ : mHistory(aHistory), mURI(aURI) {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD
+ OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override {
+ Maybe<bool> visitedState = GetVisitedValue(aCx, aData);
+ JS_ClearPendingException(aCx);
+ if (visitedState) {
+ AutoTArray<VisitedURI, 1> visitedURIs;
+ visitedURIs.AppendElement(VisitedURI{mURI.get(), *visitedState});
+ mHistory->HandleVisitedState(visitedURIs, nullptr);
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override {
+ return NS_OK;
+ }
+
+ private:
+ virtual ~OnVisitedCallback() {}
+
+ Maybe<bool> GetVisitedValue(JSContext* aCx, JS::Handle<JS::Value> aData) {
+ if (NS_WARN_IF(!aData.isBoolean())) {
+ return Nothing();
+ }
+ return Some(aData.toBoolean());
+ }
+
+ RefPtr<GeckoViewHistory> mHistory;
+ nsCOMPtr<nsIURI> mURI;
+};
+
+NS_IMPL_ISUPPORTS(OnVisitedCallback, nsIAndroidEventCallback)
+
+NS_IMETHODIMP
+GeckoViewHistory::VisitURI(nsIWidget* aWidget, nsIURI* aURI,
+ nsIURI* aLastVisitedURI, uint32_t aFlags,
+ uint64_t aBrowserId) {
+ if (!aURI) {
+ return NS_OK;
+ }
+
+ if (XRE_IsContentProcess()) {
+ // If we're in the content process, send the visit to the parent. The parent
+ // will find the matching chrome window for the content process and tab,
+ // then forward the visit to Java.
+ if (NS_WARN_IF(!aWidget)) {
+ return NS_OK;
+ }
+ BrowserChild* browserChild = aWidget->GetOwningBrowserChild();
+ if (NS_WARN_IF(!browserChild)) {
+ return NS_OK;
+ }
+ Unused << NS_WARN_IF(
+ !browserChild->SendVisitURI(aURI, aLastVisitedURI, aFlags, aBrowserId));
+ return NS_OK;
+ }
+
+ // Otherwise, we're in the parent process. Wrap the URIs up in a bundle, and
+ // send them to Java.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ RefPtr<nsWindow> window = nsWindow::From(aWidget);
+ if (NS_WARN_IF(!window)) {
+ return NS_OK;
+ }
+ widget::EventDispatcher* dispatcher = window->GetEventDispatcher();
+ if (NS_WARN_IF(!dispatcher)) {
+ return NS_OK;
+ }
+
+ // If nobody is listening for this, we can stop now.
+ if (!dispatcher->HasListener(kOnVisitedMessage)) {
+ return NS_OK;
+ }
+
+ AutoTArray<jni::String::LocalRef, 3> keys;
+ AutoTArray<jni::Object::LocalRef, 3> values;
+
+ nsAutoCString uriSpec;
+ if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(uriSpec)))) {
+ return NS_OK;
+ }
+ keys.AppendElement(jni::StringParam(u"url"_ns));
+ values.AppendElement(jni::StringParam(uriSpec));
+
+ if (aLastVisitedURI) {
+ nsAutoCString lastVisitedURISpec;
+ if (NS_WARN_IF(NS_FAILED(aLastVisitedURI->GetSpec(lastVisitedURISpec)))) {
+ return NS_OK;
+ }
+ keys.AppendElement(jni::StringParam(u"lastVisitedURL"_ns));
+ values.AppendElement(jni::StringParam(lastVisitedURISpec));
+ }
+
+ int32_t flags = 0;
+ if (aFlags & TOP_LEVEL) {
+ flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_TOP_LEVEL);
+ }
+ if (aFlags & REDIRECT_TEMPORARY) {
+ flags |=
+ static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_TEMPORARY);
+ }
+ if (aFlags & REDIRECT_PERMANENT) {
+ flags |=
+ static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_PERMANENT);
+ }
+ if (aFlags & REDIRECT_SOURCE) {
+ flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE);
+ }
+ if (aFlags & REDIRECT_SOURCE_PERMANENT) {
+ flags |= static_cast<int32_t>(
+ GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE_PERMANENT);
+ }
+ if (aFlags & UNRECOVERABLE_ERROR) {
+ flags |=
+ static_cast<int32_t>(GeckoViewVisitFlags::VISIT_UNRECOVERABLE_ERROR);
+ }
+ keys.AppendElement(jni::StringParam(u"flags"_ns));
+ values.AppendElement(java::sdk::Integer::ValueOf(flags));
+
+ MOZ_ASSERT(keys.Length() == values.Length());
+
+ auto bundleKeys = jni::ObjectArray::New<jni::String>(keys.Length());
+ auto bundleValues = jni::ObjectArray::New<jni::Object>(values.Length());
+ for (size_t i = 0; i < keys.Length(); ++i) {
+ bundleKeys->SetElement(i, keys[i]);
+ bundleValues->SetElement(i, values[i]);
+ }
+ auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
+
+ nsCOMPtr<nsIAndroidEventCallback> callback =
+ new OnVisitedCallback(this, aURI);
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(dispatcher->Dispatch(kOnVisitedMessage, bundle, callback)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoViewHistory::SetURITitle(nsIURI* aURI, const nsAString& aTitle) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/**
+ * Called from the session handler for the history delegate, with visited
+ * statuses for all requested URIs.
+ */
+class GetVisitedCallback final : public nsIAndroidEventCallback {
+ public:
+ explicit GetVisitedCallback(GeckoViewHistory* aHistory,
+ ContentParent* aInterestedProcess,
+ nsTArray<RefPtr<nsIURI>>&& aURIs)
+ : mHistory(aHistory),
+ mInterestedProcess(aInterestedProcess),
+ mURIs(std::move(aURIs)) {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD
+ OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override {
+ nsTArray<VisitedURI> visitedURIs;
+ if (!ExtractVisitedURIs(aCx, aData, visitedURIs)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+ IHistory::ContentParentSet interestedProcesses;
+ if (mInterestedProcess) {
+ interestedProcesses.Insert(mInterestedProcess);
+ }
+ mHistory->HandleVisitedState(visitedURIs, &interestedProcesses);
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override {
+ return NS_OK;
+ }
+
+ private:
+ virtual ~GetVisitedCallback() {}
+
+ /**
+ * Unpacks an array of Boolean visited statuses from the session handler into
+ * an array of `VisitedURI` structs. Each element in the array corresponds to
+ * a URI in `mURIs`.
+ *
+ * Returns `false` on error, `true` if the array is `null` or was successfully
+ * unpacked.
+ *
+ * TODO (bug 1503482): Remove this unboxing.
+ */
+ bool ExtractVisitedURIs(JSContext* aCx, JS::Handle<JS::Value> aData,
+ nsTArray<VisitedURI>& aVisitedURIs) {
+ if (aData.isNull()) {
+ return true;
+ }
+ bool isArray = false;
+ if (NS_WARN_IF(!JS::IsArrayObject(aCx, aData, &isArray))) {
+ return false;
+ }
+ if (NS_WARN_IF(!isArray)) {
+ return false;
+ }
+ JS::Rooted<JSObject*> visited(aCx, &aData.toObject());
+ uint32_t length = 0;
+ if (NS_WARN_IF(!JS::GetArrayLength(aCx, visited, &length))) {
+ return false;
+ }
+ if (NS_WARN_IF(length != mURIs.Length())) {
+ return false;
+ }
+ if (!aVisitedURIs.SetCapacity(length, mozilla::fallible)) {
+ return false;
+ }
+ for (uint32_t i = 0; i < length; ++i) {
+ JS::Rooted<JS::Value> value(aCx);
+ if (NS_WARN_IF(!JS_GetElement(aCx, visited, i, &value))) {
+ JS_ClearPendingException(aCx);
+ aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false});
+ continue;
+ }
+ if (NS_WARN_IF(!value.isBoolean())) {
+ aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false});
+ continue;
+ }
+ aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), value.toBoolean()});
+ }
+ return true;
+ }
+
+ RefPtr<GeckoViewHistory> mHistory;
+ RefPtr<ContentParent> mInterestedProcess;
+ nsTArray<RefPtr<nsIURI>> mURIs;
+};
+
+NS_IMPL_ISUPPORTS(GetVisitedCallback, nsIAndroidEventCallback)
+
+/**
+ * Queries the history delegate to find which URIs have been visited. This
+ * is always called in the parent process: from `GetVisited` in non-e10s, and
+ * from `ContentParent::RecvGetVisited` in e10s.
+ */
+void GeckoViewHistory::QueryVisitedState(nsIWidget* aWidget,
+ ContentParent* aInterestedProcess,
+ nsTArray<RefPtr<nsIURI>>&& aURIs) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ RefPtr<nsWindow> window = nsWindow::From(aWidget);
+ if (NS_WARN_IF(!window)) {
+ return;
+ }
+ widget::EventDispatcher* dispatcher = window->GetEventDispatcher();
+ if (NS_WARN_IF(!dispatcher)) {
+ return;
+ }
+
+ // If nobody is listening for this we can stop now
+ if (!dispatcher->HasListener(kGetVisitedMessage)) {
+ return;
+ }
+
+ // Assemble a bundle like `{ urls: ["http://example.com/1", ...] }`.
+ auto uris = jni::ObjectArray::New<jni::String>(aURIs.Length());
+ for (size_t i = 0; i < aURIs.Length(); ++i) {
+ nsAutoCString uriSpec;
+ if (NS_WARN_IF(NS_FAILED(aURIs[i]->GetSpec(uriSpec)))) {
+ continue;
+ }
+ jni::String::LocalRef value{jni::StringParam(uriSpec)};
+ uris->SetElement(i, value);
+ }
+
+ auto bundleKeys = jni::ObjectArray::New<jni::String>(1);
+ jni::String::LocalRef key(jni::StringParam(u"urls"_ns));
+ bundleKeys->SetElement(0, key);
+
+ auto bundleValues = jni::ObjectArray::New<jni::Object>(1);
+ jni::Object::LocalRef value(uris);
+ bundleValues->SetElement(0, value);
+
+ auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues);
+
+ nsCOMPtr<nsIAndroidEventCallback> callback =
+ new GetVisitedCallback(this, aInterestedProcess, std::move(aURIs));
+
+ Unused << NS_WARN_IF(
+ NS_FAILED(dispatcher->Dispatch(kGetVisitedMessage, bundle, callback)));
+}
+
+/**
+ * Updates link states for all tracked links, forwarding the visited statuses to
+ * the content process in e10s. This is always called in the parent process,
+ * from `VisitedCallback::OnSuccess` and `GetVisitedCallback::OnSuccess`.
+ */
+void GeckoViewHistory::HandleVisitedState(
+ const nsTArray<VisitedURI>& aVisitedURIs,
+ ContentParentSet* aInterestedProcesses) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ for (const VisitedURI& visitedURI : aVisitedURIs) {
+ auto status =
+ visitedURI.mVisited ? VisitedStatus::Visited : VisitedStatus::Unvisited;
+ NotifyVisited(visitedURI.mURI, status, aInterestedProcesses);
+ }
+}
diff --git a/mobile/android/components/geckoview/GeckoViewHistory.h b/mobile/android/components/geckoview/GeckoViewHistory.h
new file mode 100644
index 0000000000..a3b96ba58f
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewHistory.h
@@ -0,0 +1,60 @@
+/* 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/. */
+
+#ifndef GECKOVIEWHISTORY_H
+#define GECKOVIEWHISTORY_H
+
+#include "mozilla/BaseHistory.h"
+#include "nsTObserverArray.h"
+#include "nsURIHashKey.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsIURI.h"
+
+#include "mozilla/StaticPtr.h"
+
+class nsIWidget;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+} // namespace mozilla
+
+struct VisitedURI {
+ nsCOMPtr<nsIURI> mURI;
+ bool mVisited = false;
+};
+
+class GeckoViewHistory final : public mozilla::BaseHistory {
+ public:
+ NS_DECL_ISUPPORTS
+
+ // IHistory
+ NS_IMETHOD VisitURI(nsIWidget*, nsIURI*, nsIURI* aLastVisitedURI,
+ uint32_t aFlags, uint64_t aBrowserId) final;
+ NS_IMETHOD SetURITitle(nsIURI*, const nsAString&) final;
+
+ static already_AddRefed<GeckoViewHistory> GetSingleton();
+
+ void StartPendingVisitedQueries(PendingVisitedQueries&&) final;
+
+ GeckoViewHistory();
+
+ void QueryVisitedState(nsIWidget* aWidget,
+ mozilla::dom::ContentParent* aInterestedProcess,
+ nsTArray<RefPtr<nsIURI>>&& aURIs);
+ void HandleVisitedState(const nsTArray<VisitedURI>& aVisitedURIs,
+ ContentParentSet* aInterestedProcesses);
+
+ private:
+ virtual ~GeckoViewHistory();
+
+ void QueryVisitedStateInContentProcess(const PendingVisitedQueries&);
+ void QueryVisitedStateInParentProcess(const PendingVisitedQueries&);
+
+ static mozilla::StaticRefPtr<GeckoViewHistory> sHistory;
+};
+
+#endif
diff --git a/mobile/android/components/geckoview/GeckoViewOutputStream.cpp b/mobile/android/components/geckoview/GeckoViewOutputStream.cpp
new file mode 100644
index 0000000000..6368363f59
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewOutputStream.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#include "GeckoViewOutputStream.h"
+#include "mozilla/fallible.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(GeckoViewOutputStream, nsIOutputStream);
+
+NS_IMETHODIMP
+GeckoViewOutputStream::Close() {
+ mStream->SendEof();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoViewOutputStream::Flush() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+GeckoViewOutputStream::StreamStatus() {
+ return mStream->IsStreamClosed() ? NS_BASE_STREAM_CLOSED : NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoViewOutputStream::Write(const char* buf, uint32_t count,
+ uint32_t* retval) {
+ jni::ByteArray::LocalRef buffer = jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(buf), count, fallible);
+ if (!buffer) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (NS_FAILED(mStream->AppendBuffer(buffer))) {
+ // The stream was closed, abort reading this channel.
+ return NS_BASE_STREAM_CLOSED;
+ }
+ // Return amount of bytes written
+ *retval = count;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoViewOutputStream::WriteFrom(nsIInputStream* fromStream, uint32_t count,
+ uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+GeckoViewOutputStream::WriteSegments(nsReadSegmentFun reader, void* closure,
+ uint32_t count, uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+GeckoViewOutputStream::IsNonBlocking(bool* retval) {
+ *retval = true;
+ return NS_OK;
+}
diff --git a/mobile/android/components/geckoview/GeckoViewOutputStream.h b/mobile/android/components/geckoview/GeckoViewOutputStream.h
new file mode 100644
index 0000000000..70ab8a9198
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewOutputStream.h
@@ -0,0 +1,29 @@
+
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef GeckoViewOutputStream_h__
+#define GeckoViewOutputStream_h__
+
+#include "mozilla/java/GeckoInputStreamNatives.h"
+#include "mozilla/java/GeckoInputStreamWrappers.h"
+
+#include "nsIOutputStream.h"
+#include "nsIRequest.h"
+
+class GeckoViewOutputStream : public nsIOutputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOUTPUTSTREAM
+ explicit GeckoViewOutputStream(
+ mozilla::java::GeckoInputStream::GlobalRef aStream)
+ : mStream(aStream) {}
+
+ private:
+ const mozilla::java::GeckoInputStream::GlobalRef mStream;
+ virtual ~GeckoViewOutputStream() = default;
+};
+
+#endif // GeckoViewOutputStream_h__
diff --git a/mobile/android/components/geckoview/GeckoViewPermission.jsm b/mobile/android/components/geckoview/GeckoViewPermission.jsm
new file mode 100644
index 0000000000..88ab64ab9d
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewPermission.jsm
@@ -0,0 +1,41 @@
+/* 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 = ["GeckoViewPermission"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+class GeckoViewPermission {
+ constructor() {
+ this.wrappedJSObject = this;
+ }
+
+ async prompt(aRequest) {
+ const window = aRequest.window
+ ? aRequest.window
+ : aRequest.element.ownerGlobal;
+
+ const actor = window.windowGlobalChild.getActor("GeckoViewPermission");
+ const result = await actor.promptPermission(aRequest);
+ if (!result.allow) {
+ aRequest.cancel();
+ } else {
+ // Note: permission could be undefined, that's what aRequest expects.
+ const { permission } = result;
+ aRequest.allow(permission);
+ }
+ }
+}
+
+GeckoViewPermission.prototype.classID = Components.ID(
+ "{42f3c238-e8e8-4015-9ca2-148723a8afcf}"
+);
+GeckoViewPermission.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIContentPermissionPrompt",
+]);
+
+const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPermission");
diff --git a/mobile/android/components/geckoview/GeckoViewPrompt.jsm b/mobile/android/components/geckoview/GeckoViewPrompt.jsm
new file mode 100644
index 0000000000..af488279c9
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewPrompt.jsm
@@ -0,0 +1,826 @@
+/* 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 = ["PromptFactory"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
+});
+
+const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt");
+
+class PromptFactory {
+ constructor() {
+ this.wrappedJSObject = this;
+ }
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "mozshowdropdown":
+ case "mozshowdropdown-sourcetouch":
+ this._handleSelect(aEvent.composedTarget, /* aIsDropDown = */ true);
+ break;
+ case "MozOpenDateTimePicker":
+ this._handleDateTime(aEvent.composedTarget);
+ break;
+ case "click":
+ this._handleClick(aEvent);
+ break;
+ case "DOMPopupBlocked":
+ this._handlePopupBlocked(aEvent);
+ break;
+ }
+ }
+
+ _handleClick(aEvent) {
+ const target = aEvent.composedTarget;
+ const className = ChromeUtils.getClassName(target);
+ if (className !== "HTMLInputElement" && className !== "HTMLSelectElement") {
+ return;
+ }
+
+ if (
+ target.isContentEditable ||
+ target.disabled ||
+ target.readOnly ||
+ !target.willValidate
+ ) {
+ // target.willValidate is false when any associated fieldset is disabled,
+ // in which case this element is treated as disabled per spec.
+ return;
+ }
+
+ if (className === "HTMLSelectElement") {
+ if (!target.isCombobox) {
+ this._handleSelect(target, /* aIsDropDown = */ false);
+ return;
+ }
+ // combobox select is handled by mozshowdropdown.
+ return;
+ }
+
+ const type = target.type;
+ if (type === "month" || type === "week") {
+ // If there's a shadow root, the MozOpenDateTimePicker event takes care
+ // of this. Right now for these input types there's never a shadow root.
+ // Once we support UA widgets for month/week inputs (see bug 888320), we
+ // can remove this.
+ if (!target.openOrClosedShadowRoot) {
+ this._handleDateTime(target);
+ aEvent.preventDefault();
+ }
+ }
+ }
+
+ _generateSelectItems(aElement) {
+ const win = aElement.ownerGlobal;
+ let id = 0;
+ const map = {};
+
+ const items = (function enumList(elem, disabled) {
+ const items = [];
+ const children = elem.children;
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ if (win.getComputedStyle(child).display === "none") {
+ continue;
+ }
+ const item = {
+ id: String(id),
+ disabled: disabled || child.disabled,
+ };
+ if (win.HTMLOptGroupElement.isInstance(child)) {
+ item.label = child.label;
+ item.items = enumList(child, item.disabled);
+ } else if (win.HTMLOptionElement.isInstance(child)) {
+ item.label = child.label || child.text;
+ item.selected = child.selected;
+ } else {
+ continue;
+ }
+ items.push(item);
+ map[id++] = child;
+ }
+ return items;
+ })(aElement);
+
+ return [items, map, id];
+ }
+
+ _handleSelect(aElement, aIsDropDown) {
+ const win = aElement.ownerGlobal;
+ const [items] = this._generateSelectItems(aElement);
+
+ if (aIsDropDown) {
+ aElement.openInParentProcess = true;
+ }
+
+ const prompt = new lazy.GeckoViewPrompter(win);
+
+ // Something changed the <select> while it was open.
+ const deferredUpdate = new lazy.DeferredTask(() => {
+ // Inner contents in choice prompt are updated.
+ const [newItems] = this._generateSelectItems(aElement);
+ prompt.update({
+ type: "choice",
+ mode: aElement.multiple ? "multiple" : "single",
+ choices: newItems,
+ });
+ }, 0);
+ const mut = new win.MutationObserver(() => {
+ deferredUpdate.arm();
+ });
+ mut.observe(aElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ });
+
+ const dismissPrompt = () => prompt.dismiss();
+ aElement.addEventListener("blur", dismissPrompt, { mozSystemGroup: true });
+ const hidedropdown = event => {
+ if (aElement === event.target) {
+ prompt.dismiss();
+ }
+ };
+ const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler;
+ chromeEventHandler.addEventListener("mozhidedropdown", hidedropdown, {
+ mozSystemGroup: true,
+ });
+
+ prompt.asyncShowPrompt(
+ {
+ type: "choice",
+ mode: aElement.multiple ? "multiple" : "single",
+ choices: items,
+ },
+ result => {
+ deferredUpdate.disarm();
+ mut.disconnect();
+ aElement.removeEventListener("blur", dismissPrompt, {
+ mozSystemGroup: true,
+ });
+ chromeEventHandler.removeEventListener(
+ "mozhidedropdown",
+ hidedropdown,
+ { mozSystemGroup: true }
+ );
+
+ if (aIsDropDown) {
+ aElement.openInParentProcess = false;
+ }
+ // OK: result
+ // Cancel: !result
+ if (!result || result.choices === undefined) {
+ return;
+ }
+
+ const [, map, id] = this._generateSelectItems(aElement);
+ let dispatchEvents = false;
+ if (!aElement.multiple) {
+ const elem = map[result.choices[0]];
+ if (elem && win.HTMLOptionElement.isInstance(elem)) {
+ dispatchEvents = !elem.selected;
+ elem.selected = true;
+ } else {
+ console.error("Invalid id for select result: " + result.choices[0]);
+ }
+ } else {
+ for (let i = 0; i < id; i++) {
+ const elem = map[i];
+ const index = result.choices.indexOf(String(i));
+ if (
+ win.HTMLOptionElement.isInstance(elem) &&
+ elem.selected !== index >= 0
+ ) {
+ // Current selected is not the same as the new selected state.
+ dispatchEvents = true;
+ elem.selected = !elem.selected;
+ }
+ result.choices[index] = undefined;
+ }
+ for (let i = 0; i < result.choices.length; i++) {
+ if (result.choices[i] !== undefined && result.choices[i] !== null) {
+ console.error(
+ "Invalid id for select result: " + result.choices[i]
+ );
+ break;
+ }
+ }
+ }
+
+ if (dispatchEvents) {
+ this._dispatchEvents(aElement);
+ }
+ }
+ );
+ }
+
+ _handleDateTime(aElement) {
+ const win = aElement.ownerGlobal;
+ const prompt = new lazy.GeckoViewPrompter(win);
+
+ const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler;
+ const dismissPrompt = () => prompt.dismiss();
+ // Some controls don't have UA widget (bug 888320)
+ {
+ const dateTimeBoxElement = aElement.dateTimeBoxElement;
+ if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) {
+ aElement.addEventListener("blur", dismissPrompt, {
+ mozSystemGroup: true,
+ });
+ } else {
+ chromeEventHandler.addEventListener(
+ "MozCloseDateTimePicker",
+ dismissPrompt
+ );
+
+ dateTimeBoxElement.dispatchEvent(
+ new win.CustomEvent("MozSetDateTimePickerState", { detail: true })
+ );
+ }
+ }
+
+ prompt.asyncShowPrompt(
+ {
+ type: "datetime",
+ mode: aElement.type,
+ value: aElement.value,
+ min: aElement.min,
+ max: aElement.max,
+ step: aElement.step,
+ },
+ result => {
+ // Some controls don't have UA widget (bug 888320)
+ const dateTimeBoxElement = aElement.dateTimeBoxElement;
+ if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) {
+ aElement.removeEventListener("blur", dismissPrompt, {
+ mozSystemGroup: true,
+ });
+ } else {
+ chromeEventHandler.removeEventListener(
+ "MozCloseDateTimePicker",
+ dismissPrompt
+ );
+ dateTimeBoxElement.dispatchEvent(
+ new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
+ );
+ }
+
+ // OK: result
+ // Cancel: !result
+ if (
+ !result ||
+ result.datetime === undefined ||
+ result.datetime === aElement.value
+ ) {
+ return;
+ }
+ aElement.value = result.datetime;
+ this._dispatchEvents(aElement);
+ }
+ );
+ }
+
+ _dispatchEvents(aElement) {
+ // Fire both "input" and "change" events for <select> and <input> for
+ // date/time.
+ aElement.dispatchEvent(
+ new aElement.ownerGlobal.Event("input", { bubbles: true, composed: true })
+ );
+ aElement.dispatchEvent(
+ new aElement.ownerGlobal.Event("change", { bubbles: true })
+ );
+ }
+
+ _handlePopupBlocked(aEvent) {
+ const dwi = aEvent.requestingWindow;
+ const popupWindowURISpec = aEvent.popupWindowURI
+ ? aEvent.popupWindowURI.displaySpec
+ : "about:blank";
+
+ const prompt = new lazy.GeckoViewPrompter(aEvent.requestingWindow);
+ prompt.asyncShowPrompt(
+ {
+ type: "popup",
+ targetUri: popupWindowURISpec,
+ },
+ ({ response }) => {
+ if (response && dwi) {
+ dwi.open(
+ popupWindowURISpec,
+ aEvent.popupWindowName,
+ aEvent.popupWindowFeatures
+ );
+ }
+ }
+ );
+ }
+
+ /* ---------- nsIPromptFactory ---------- */
+ getPrompt(aDOMWin, aIID) {
+ // Delegated to login manager here, which in turn calls back into us via nsIPromptService.
+ if (aIID.equals(Ci.nsIAuthPrompt2) || aIID.equals(Ci.nsIAuthPrompt)) {
+ try {
+ const pwmgr = Cc[
+ "@mozilla.org/passwordmanager/authpromptfactory;1"
+ ].getService(Ci.nsIPromptFactory);
+ return pwmgr.getPrompt(aDOMWin, aIID);
+ } catch (e) {
+ console.error("Delegation to password manager failed: " + e);
+ }
+ }
+
+ const p = new PromptDelegate(aDOMWin);
+ p.QueryInterface(aIID);
+ return p;
+ }
+
+ /* ---------- private memebers ---------- */
+
+ // nsIPromptService methods proxy to our Prompt class
+ callProxy(aMethod, aArguments) {
+ const prompt = new PromptDelegate(aArguments[0]);
+ let promptArgs;
+ if (BrowsingContext.isInstance(aArguments[0])) {
+ // Called by BrowsingContext prompt method, strip modalType.
+ [, , /*browsingContext*/ /*modalType*/ ...promptArgs] = aArguments;
+ } else {
+ [, /*domWindow*/ ...promptArgs] = aArguments;
+ }
+ return prompt[aMethod].apply(prompt, promptArgs);
+ }
+
+ /* ---------- nsIPromptService ---------- */
+
+ alert() {
+ return this.callProxy("alert", arguments);
+ }
+ alertBC() {
+ return this.callProxy("alert", arguments);
+ }
+ alertCheck() {
+ return this.callProxy("alertCheck", arguments);
+ }
+ alertCheckBC() {
+ return this.callProxy("alertCheck", arguments);
+ }
+ confirm() {
+ return this.callProxy("confirm", arguments);
+ }
+ confirmBC() {
+ return this.callProxy("confirm", arguments);
+ }
+ confirmCheck() {
+ return this.callProxy("confirmCheck", arguments);
+ }
+ confirmCheckBC() {
+ return this.callProxy("confirmCheck", arguments);
+ }
+ confirmEx() {
+ return this.callProxy("confirmEx", arguments);
+ }
+ confirmExBC() {
+ return this.callProxy("confirmEx", arguments);
+ }
+ prompt() {
+ return this.callProxy("prompt", arguments);
+ }
+ promptBC() {
+ return this.callProxy("prompt", arguments);
+ }
+ promptUsernameAndPassword() {
+ return this.callProxy("promptUsernameAndPassword", arguments);
+ }
+ promptUsernameAndPasswordBC() {
+ return this.callProxy("promptUsernameAndPassword", arguments);
+ }
+ promptPassword() {
+ return this.callProxy("promptPassword", arguments);
+ }
+ promptPasswordBC() {
+ return this.callProxy("promptPassword", arguments);
+ }
+ select() {
+ return this.callProxy("select", arguments);
+ }
+ selectBC() {
+ return this.callProxy("select", arguments);
+ }
+ promptAuth() {
+ return this.callProxy("promptAuth", arguments);
+ }
+ promptAuthBC() {
+ return this.callProxy("promptAuth", arguments);
+ }
+ asyncPromptAuth() {
+ return this.callProxy("asyncPromptAuth", arguments);
+ }
+}
+
+PromptFactory.prototype.classID = Components.ID(
+ "{076ac188-23c1-4390-aa08-7ef1f78ca5d9}"
+);
+PromptFactory.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIPromptFactory",
+ "nsIPromptService",
+]);
+
+class PromptDelegate {
+ constructor(aParent) {
+ this._prompter = new lazy.GeckoViewPrompter(aParent);
+ }
+
+ BUTTON_TYPE_POSITIVE = 0;
+ BUTTON_TYPE_NEUTRAL = 1;
+ BUTTON_TYPE_NEGATIVE = 2;
+
+ /* ---------- internal methods ---------- */
+
+ _addText(aTitle, aText, aMsg) {
+ return Object.assign(aMsg, {
+ title: aTitle,
+ msg: aText,
+ });
+ }
+
+ _addCheck(aCheckMsg, aCheckState, aMsg) {
+ return Object.assign(aMsg, {
+ hasCheck: !!aCheckMsg,
+ checkMsg: aCheckMsg,
+ checkValue: aCheckState && aCheckState.value,
+ });
+ }
+
+ /* ---------- nsIPrompt ---------- */
+
+ alert(aTitle, aText) {
+ this.alertCheck(aTitle, aText);
+ }
+
+ alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ const result = this._prompter.showPrompt(
+ this._addText(
+ aTitle,
+ aText,
+ this._addCheck(aCheckMsg, aCheckState, {
+ type: "alert",
+ })
+ )
+ );
+ if (result && aCheckState) {
+ aCheckState.value = !!result.checkValue;
+ }
+ }
+
+ confirm(aTitle, aText) {
+ // Button 0 is OK.
+ return this.confirmCheck(aTitle, aText);
+ }
+
+ confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ // Button 0 is OK.
+ return (
+ this.confirmEx(
+ aTitle,
+ aText,
+ Ci.nsIPrompt.STD_OK_CANCEL_BUTTONS,
+ /* aButton0 */ null,
+ /* aButton1 */ null,
+ /* aButton2 */ null,
+ aCheckMsg,
+ aCheckState
+ ) == 0
+ );
+ }
+
+ confirmEx(
+ aTitle,
+ aText,
+ aButtonFlags,
+ aButton0,
+ aButton1,
+ aButton2,
+ aCheckMsg,
+ aCheckState
+ ) {
+ const btnMap = Array(3).fill(null);
+ const btnTitle = Array(3).fill(null);
+ const btnCustomTitle = Array(3).fill(null);
+ const savedButtonId = [];
+ for (let i = 0; i < 3; i++) {
+ const btnFlags = aButtonFlags >> (i * 8);
+ switch (btnFlags & 0xff) {
+ case Ci.nsIPrompt.BUTTON_TITLE_OK:
+ btnMap[this.BUTTON_TYPE_POSITIVE] = i;
+ btnTitle[this.BUTTON_TYPE_POSITIVE] = "ok";
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
+ btnMap[this.BUTTON_TYPE_NEGATIVE] = i;
+ btnTitle[this.BUTTON_TYPE_NEGATIVE] = "cancel";
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_YES:
+ btnMap[this.BUTTON_TYPE_POSITIVE] = i;
+ btnTitle[this.BUTTON_TYPE_POSITIVE] = "yes";
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_NO:
+ btnMap[this.BUTTON_TYPE_NEGATIVE] = i;
+ btnTitle[this.BUTTON_TYPE_NEGATIVE] = "no";
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
+ // We don't know if this is positive/negative/neutral, so save for later.
+ savedButtonId.push(i);
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
+ case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
+ case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
+ // Not supported; fall-through.
+ default:
+ break;
+ }
+ }
+
+ // Put saved buttons into available slots.
+ 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._prompter.showPrompt(
+ this._addText(
+ aTitle,
+ aText,
+ this._addCheck(aCheckMsg, aCheckState, {
+ type: "button",
+ btnTitle,
+ btnCustomTitle,
+ })
+ )
+ );
+ 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._prompter.showPrompt(
+ this._addText(
+ aTitle,
+ aText,
+ this._addCheck(aCheckMsg, aCheckState, {
+ type: "text",
+ value: aValue.value,
+ })
+ )
+ );
+ // OK: result && result.text !== undefined
+ // Cancel: result && result.text === undefined
+ // Error: !result
+ if (result && aCheckState) {
+ aCheckState.value = !!result.checkValue;
+ }
+ if (!result || result.text === undefined) {
+ return false;
+ }
+ aValue.value = result.text || "";
+ return true;
+ }
+
+ promptPassword(aTitle, aText, aPassword) {
+ return this._promptUsernameAndPassword(
+ aTitle,
+ aText,
+ /* aUsername */ undefined,
+ aPassword
+ );
+ }
+
+ promptUsernameAndPassword(aTitle, aText, aUsername, aPassword) {
+ const msg = {
+ 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._prompter.showPrompt(this._addText(aTitle, aText, msg));
+ // OK: result && result.password !== undefined
+ // Cancel: result && result.password === undefined
+ // Error: !result
+ if (!result || result.password === undefined) {
+ return false;
+ }
+ if (aUsername) {
+ aUsername.value = result.username || "";
+ }
+ aPassword.value = result.password || "";
+ return true;
+ }
+
+ 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._prompter.showPrompt(
+ this._addText(aTitle, aText, {
+ type: "choice",
+ mode: "single",
+ choices,
+ })
+ );
+ // OK: result
+ // Cancel: !result
+ if (!result || result.choices === undefined) {
+ return false;
+ }
+ aOutSelection.value = Number(result.choices[0]);
+ return true;
+ }
+
+ _getAuthMsg(aChannel, aLevel, aAuthInfo) {
+ let username;
+ if (
+ aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN &&
+ aAuthInfo.domain
+ ) {
+ username = aAuthInfo.domain + "\\" + aAuthInfo.username;
+ } else {
+ username = aAuthInfo.username;
+ }
+ return this._addText(
+ /* title */ null,
+ this._getAuthText(aChannel, aAuthInfo),
+ {
+ type: "auth",
+ mode:
+ aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD
+ ? "password"
+ : "auth",
+ options: {
+ flags: aAuthInfo.flags,
+ uri: aChannel && aChannel.URI.displaySpec,
+ level: aLevel,
+ username,
+ password: aAuthInfo.password,
+ },
+ }
+ );
+ }
+
+ _fillAuthInfo(aAuthInfo, aResult) {
+ if (!aResult || aResult.password === undefined) {
+ return false;
+ }
+
+ aAuthInfo.password = aResult.password || "";
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
+ return true;
+ }
+
+ const username = aResult.username || "";
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ var idx = username.indexOf("\\");
+ if (idx >= 0) {
+ aAuthInfo.domain = username.substring(0, idx);
+ aAuthInfo.username = username.substring(idx + 1);
+ return true;
+ }
+ }
+ aAuthInfo.username = username;
+ return true;
+ }
+
+ promptAuth(aChannel, aLevel, aAuthInfo) {
+ const result = this._prompter.showPrompt(
+ this._getAuthMsg(aChannel, aLevel, aAuthInfo)
+ );
+ // OK: result && result.password !== undefined
+ // Cancel: result && result.password === undefined
+ // Error: !result
+ return this._fillAuthInfo(aAuthInfo, result);
+ }
+
+ async asyncPromptAuth(aChannel, aLevel, aAuthInfo) {
+ const result = await this._prompter.asyncShowPromptPromise(
+ this._getAuthMsg(aChannel, aLevel, aAuthInfo)
+ );
+ // OK: result && result.password !== undefined
+ // Cancel: result && result.password === undefined
+ // Error: !result
+ return this._fillAuthInfo(aAuthInfo, result);
+ }
+
+ _getAuthText(aChannel, aAuthInfo) {
+ const isProxy = aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY;
+ const isPassOnly = aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD;
+ const isCrossOrig =
+ aAuthInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE;
+
+ const username = aAuthInfo.username;
+ const authTarget = this._getAuthTarget(aChannel, aAuthInfo);
+ const { displayHost } = authTarget;
+ let { realm } = authTarget;
+
+ // Suppress "the site says: $realm" when we synthesized a missing realm.
+ if (!aAuthInfo.realm && !isProxy) {
+ realm = "";
+ }
+
+ // Trim obnoxiously long realms.
+ if (realm.length > 50) {
+ realm = realm.substring(0, 50) + "\u2026";
+ }
+
+ const bundle = Services.strings.createBundle(
+ "chrome://global/locale/commonDialogs.properties"
+ );
+ let text;
+ if (isProxy) {
+ text = bundle.formatStringFromName("EnterLoginForProxy3", [
+ realm,
+ displayHost,
+ ]);
+ } else if (isPassOnly) {
+ text = bundle.formatStringFromName("EnterPasswordFor", [
+ username,
+ displayHost,
+ ]);
+ } else if (isCrossOrig) {
+ text = bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [
+ displayHost,
+ ]);
+ } else if (!realm) {
+ text = bundle.formatStringFromName("EnterUserPasswordFor2", [
+ displayHost,
+ ]);
+ } else {
+ text = bundle.formatStringFromName("EnterLoginForRealm3", [
+ realm,
+ displayHost,
+ ]);
+ }
+
+ return text;
+ }
+
+ _getAuthTarget(aChannel, aAuthInfo) {
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ if (!(aChannel instanceof Ci.nsIProxiedChannel)) {
+ throw new Error("proxy auth needs nsIProxiedChannel");
+ }
+ const info = aChannel.proxyInfo;
+ if (!info) {
+ throw new Error("proxy auth needs nsIProxyInfo");
+ }
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ const idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ const displayHost =
+ "moz-proxy://" +
+ idnService.convertUTF8toACE(info.host) +
+ ":" +
+ info.port;
+ let realm = aAuthInfo.realm;
+ if (!realm) {
+ realm = displayHost;
+ }
+ return { displayHost, realm };
+ }
+
+ const displayHost =
+ aChannel.URI.scheme + "://" + aChannel.URI.displayHostPort;
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ let realm = aAuthInfo.realm;
+ if (!realm) {
+ realm = displayHost;
+ }
+ return { displayHost, realm };
+ }
+}
+
+PromptDelegate.prototype.QueryInterface = ChromeUtils.generateQI(["nsIPrompt"]);
diff --git a/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs
new file mode 100644
index 0000000000..a36ae0abec
--- /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);
+ }
+}
diff --git a/mobile/android/components/geckoview/GeckoViewPush.jsm b/mobile/android/components/geckoview/GeckoViewPush.jsm
new file mode 100644
index 0000000000..5899bfd3d8
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewPush.jsm
@@ -0,0 +1,257 @@
+/* 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 = ["PushService"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPush");
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
+});
+
+// Observer notification topics for push messages and subscription status
+// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
+// on `nsIPushService` so that JS callers only need to import this service.
+const OBSERVER_TOPIC_PUSH = "push-message";
+const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
+const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
+
+function createSubscription({
+ scope,
+ principal,
+ browserPublicKey,
+ authSecret,
+ endpoint,
+ appServerKey,
+}) {
+ const decodedBrowserKey = ChromeUtils.base64URLDecode(browserPublicKey, {
+ padding: "ignore",
+ });
+ const decodedAuthSecret = ChromeUtils.base64URLDecode(authSecret, {
+ padding: "ignore",
+ });
+
+ return new PushSubscription({
+ endpoint,
+ scope,
+ p256dhKey: decodedBrowserKey,
+ authenticationSecret: decodedAuthSecret,
+ appServerKey,
+ });
+}
+
+function scopeWithAttrs(scope, attrs) {
+ return scope + ChromeUtils.originAttributesToSuffix(attrs);
+}
+
+class PushService {
+ constructor() {
+ this.wrappedJSObject = this;
+ }
+
+ pushTopic = OBSERVER_TOPIC_PUSH;
+ subscriptionChangeTopic = OBSERVER_TOPIC_SUBSCRIPTION_CHANGE;
+ subscriptionModifiedTopic = OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED;
+
+ // nsIObserver methods
+
+ observe(subject, topic, data) {}
+
+ // nsIPushService methods
+
+ subscribe(scope, principal, callback) {
+ this.subscribeWithKey(scope, principal, null, callback);
+ }
+
+ async subscribeWithKey(scope, principal, appServerKey, callback) {
+ try {
+ const response = await lazy.EventDispatcher.instance.sendRequestForResult(
+ {
+ type: "GeckoView:PushSubscribe",
+ scope: scopeWithAttrs(scope, principal.originAttributes),
+ appServerKey: appServerKey
+ ? ChromeUtils.base64URLEncode(new Uint8Array(appServerKey), {
+ pad: true,
+ })
+ : null,
+ }
+ );
+
+ let subscription = null;
+ if (response) {
+ subscription = createSubscription({
+ ...response,
+ scope,
+ principal,
+ appServerKey,
+ });
+ }
+
+ callback.onPushSubscription(Cr.NS_OK, subscription);
+ } catch (e) {
+ callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
+ }
+ }
+
+ async unsubscribe(scope, principal, callback) {
+ try {
+ await lazy.EventDispatcher.instance.sendRequestForResult({
+ type: "GeckoView:PushUnsubscribe",
+ scope: scopeWithAttrs(scope, principal.originAttributes),
+ });
+
+ callback.onUnsubscribe(Cr.NS_OK, true);
+ } catch (e) {
+ callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
+ }
+ }
+
+ async getSubscription(scope, principal, callback) {
+ try {
+ const response = await lazy.EventDispatcher.instance.sendRequestForResult(
+ {
+ type: "GeckoView:PushGetSubscription",
+ scope: scopeWithAttrs(scope, principal.originAttributes),
+ }
+ );
+
+ let subscription = null;
+ if (response) {
+ subscription = createSubscription({
+ ...response,
+ scope,
+ principal,
+ });
+ }
+
+ callback.onPushSubscription(Cr.NS_OK, subscription);
+ } catch (e) {
+ callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null);
+ }
+ }
+
+ clearForDomain(domain, callback) {
+ callback.onClear(Cr.NS_OK);
+ }
+
+ // nsIPushQuotaManager methods
+
+ notificationForOriginShown(origin) {}
+
+ notificationForOriginClosed(origin) {}
+
+ // nsIPushErrorReporter methods
+
+ reportDeliveryError(messageId, reason) {}
+}
+
+PushService.prototype.classID = Components.ID(
+ "{a54d84d7-98a4-4fec-b664-e42e512ae9cc}"
+);
+PushService.prototype.contractID = "@mozilla.org/push/Service;1";
+PushService.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ "nsIPushService",
+ "nsIPushQuotaManager",
+ "nsIPushErrorReporter",
+]);
+
+/** `PushSubscription` instances are passed to all subscription callbacks. */
+class PushSubscription {
+ constructor(props) {
+ this._props = props;
+ }
+
+ /** The URL for sending messages to this subscription. */
+ get endpoint() {
+ return this._props.endpoint;
+ }
+
+ /** The last time a message was sent to this subscription. */
+ get lastPush() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /** The total number of messages sent to this subscription. */
+ get pushCount() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /**
+ * The app will take care of throttling, so we don't
+ * care about the quota stuff here.
+ */
+ get quota() {
+ return -1;
+ }
+
+ /**
+ * Indicates whether this subscription was created with the system principal.
+ * System subscriptions are exempt from the background message quota and
+ * permission checks.
+ */
+ get isSystemSubscription() {
+ return false;
+ }
+
+ /** The private key used to decrypt incoming push messages, in JWK format */
+ get p256dhPrivateKey() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /**
+ * Indicates whether this subscription is subject to the background message
+ * quota.
+ */
+ quotaApplies() {
+ return false;
+ }
+
+ /**
+ * Indicates whether this subscription exceeded the background message quota,
+ * or the user revoked the notification permission. The caller must request a
+ * new subscription to continue receiving push messages.
+ */
+ isExpired() {
+ return false;
+ }
+
+ /**
+ * Returns a key for encrypting messages sent to this subscription. JS
+ * callers receive the key buffer as a return value, while C++ callers
+ * receive the key size and buffer as out parameters.
+ */
+ getKey(name) {
+ switch (name) {
+ case "p256dh":
+ return this._getRawKey(this._props.p256dhKey);
+
+ case "auth":
+ return this._getRawKey(this._props.authenticationSecret);
+
+ case "appServer":
+ return this._getRawKey(this._props.appServerKey);
+ }
+ return [];
+ }
+
+ _getRawKey(key) {
+ if (!key) {
+ return [];
+ }
+ return new Uint8Array(key);
+ }
+}
+
+PushSubscription.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIPushSubscription",
+]);
diff --git a/mobile/android/components/geckoview/GeckoViewStartup.jsm b/mobile/android/components/geckoview/GeckoViewStartup.jsm
new file mode 100644
index 0000000000..07924b6e07
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewStartup.jsm
@@ -0,0 +1,344 @@
+/* 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 = ["GeckoViewStartup"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ActorManagerParent: "resource://gre/modules/ActorManagerParent.sys.mjs",
+ EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
+ PdfJs: "resource://pdf.js/PdfJs.sys.mjs",
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const { debug, warn } = GeckoViewUtils.initLogging("Startup");
+
+var { DelayedInit } = ChromeUtils.import(
+ "resource://gre/modules/DelayedInit.jsm"
+);
+
+function InitLater(fn, object, name) {
+ return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */);
+}
+
+const JSPROCESSACTORS = {
+ GeckoViewPermissionProcess: {
+ parent: {
+ esModuleURI:
+ "resource:///actors/GeckoViewPermissionProcessParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/GeckoViewPermissionProcessChild.sys.mjs",
+ observers: [
+ "getUserMedia:ask-device-permission",
+ "getUserMedia:request",
+ "recording-device-events",
+ "PeerConnection:request",
+ ],
+ },
+ },
+};
+
+const JSWINDOWACTORS = {
+ LoadURIDelegate: {
+ parent: {
+ esModuleURI: "resource:///actors/LoadURIDelegateParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/LoadURIDelegateChild.sys.mjs",
+ },
+ messageManagerGroups: ["browsers"],
+ },
+ GeckoViewPermission: {
+ parent: {
+ esModuleURI: "resource:///actors/GeckoViewPermissionParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/GeckoViewPermissionChild.sys.mjs",
+ },
+ allFrames: true,
+ includeChrome: true,
+ },
+ GeckoViewPrompt: {
+ child: {
+ esModuleURI: "resource:///actors/GeckoViewPromptChild.sys.mjs",
+ events: {
+ click: { capture: false, mozSystemGroup: true },
+ contextmenu: { capture: false, mozSystemGroup: true },
+ mozshowdropdown: {},
+ "mozshowdropdown-sourcetouch": {},
+ MozOpenDateTimePicker: {},
+ DOMPopupBlocked: { capture: false, mozSystemGroup: true },
+ },
+ },
+ allFrames: true,
+ messageManagerGroups: ["browsers"],
+ },
+ GeckoViewFormValidation: {
+ child: {
+ esModuleURI: "resource:///actors/GeckoViewFormValidationChild.sys.mjs",
+ events: {
+ MozInvalidForm: {},
+ },
+ },
+ allFrames: true,
+ messageManagerGroups: ["browsers"],
+ },
+ GeckoViewClipboardPermission: {
+ parent: {
+ esModuleURI:
+ "resource:///actors/GeckoViewClipboardPermissionParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ "resource:///actors/GeckoViewClipboardPermissionChild.sys.mjs",
+ events: {
+ MozClipboardReadPaste: {},
+ deactivate: { mozSystemGroup: true },
+ mousedown: { capture: true, mozSystemGroup: true },
+ mozvisualscroll: { mozSystemGroup: true },
+ pagehide: { capture: true, mozSystemGroup: true },
+ },
+ },
+ allFrames: true,
+ },
+ GeckoViewPdfjs: {
+ parent: {
+ esModuleURI: "resource://pdf.js/GeckoViewPdfjsParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://pdf.js/GeckoViewPdfjsChild.sys.mjs",
+ },
+ allFrames: true,
+ },
+};
+
+class GeckoViewStartup {
+ /* ---------- nsIObserver ---------- */
+ observe(aSubject, aTopic, aData) {
+ debug`observe: ${aTopic}`;
+ switch (aTopic) {
+ case "content-process-ready-for-script":
+ case "app-startup": {
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", {
+ module: "resource://gre/modules/GeckoViewConsole.sys.mjs",
+ });
+
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewStorageController", {
+ module: "resource://gre/modules/GeckoViewStorageController.sys.mjs",
+ ged: [
+ "GeckoView:ClearData",
+ "GeckoView:ClearSessionContextData",
+ "GeckoView:ClearHostData",
+ "GeckoView:ClearBaseDomainData",
+ "GeckoView:GetAllPermissions",
+ "GeckoView:GetPermissionsByURI",
+ "GeckoView:SetPermission",
+ "GeckoView:SetPermissionByURI",
+ "GeckoView:GetCookieBannerModeForDomain",
+ "GeckoView:SetCookieBannerModeForDomain",
+ "GeckoView:RemoveCookieBannerModeForDomain",
+ ],
+ });
+
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewPushController", {
+ module: "resource://gre/modules/GeckoViewPushController.sys.mjs",
+ ged: ["GeckoView:PushEvent", "GeckoView:PushSubscriptionChanged"],
+ });
+
+ GeckoViewUtils.addLazyPrefObserver(
+ {
+ name: "geckoview.console.enabled",
+ default: false,
+ },
+ {
+ handler: _ => this.GeckoViewConsole,
+ }
+ );
+
+ // Parent process only
+ if (
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT
+ ) {
+ lazy.ActorManagerParent.addJSWindowActors(JSWINDOWACTORS);
+ lazy.ActorManagerParent.addJSProcessActors(JSPROCESSACTORS);
+
+ if (Services.appinfo.sessionHistoryInParent) {
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewSessionStore", {
+ module: "resource://gre/modules/GeckoViewSessionStore.sys.mjs",
+ observers: [
+ "browsing-context-did-set-embedder",
+ "browsing-context-discarded",
+ ],
+ });
+ }
+
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
+ module: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
+ ged: [
+ "GeckoView:ActionDelegate:Attached",
+ "GeckoView:BrowserAction:Click",
+ "GeckoView:PageAction:Click",
+ "GeckoView:RegisterWebExtension",
+ "GeckoView:UnregisterWebExtension",
+ "GeckoView:WebExtension:CancelInstall",
+ "GeckoView:WebExtension:Disable",
+ "GeckoView:WebExtension:Enable",
+ "GeckoView:WebExtension:EnsureBuiltIn",
+ "GeckoView:WebExtension:Get",
+ "GeckoView:WebExtension:Install",
+ "GeckoView:WebExtension:InstallBuiltIn",
+ "GeckoView:WebExtension:List",
+ "GeckoView:WebExtension:PortDisconnect",
+ "GeckoView:WebExtension:PortMessageFromApp",
+ "GeckoView:WebExtension:SetPBAllowed",
+ "GeckoView:WebExtension:Uninstall",
+ "GeckoView:WebExtension:Update",
+ ],
+ observers: [
+ "devtools-installed-addon",
+ "testing-installed-addon",
+ "testing-uninstalled-addon",
+ ],
+ });
+
+ GeckoViewUtils.addLazyGetter(this, "ChildCrashHandler", {
+ module: "resource://gre/modules/ChildCrashHandler.sys.mjs",
+ observers: ["ipc:content-shutdown", "compositor:process-aborted"],
+ });
+
+ lazy.EventDispatcher.instance.registerListener(this, [
+ "GeckoView:StorageDelegate:Attached",
+ ]);
+ }
+ break;
+ }
+
+ case "profile-after-change": {
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewRemoteDebugger", {
+ module: "resource://gre/modules/GeckoViewRemoteDebugger.sys.mjs",
+ init: gvrd => gvrd.onInit(),
+ });
+
+ GeckoViewUtils.addLazyPrefObserver(
+ {
+ name: "devtools.debugger.remote-enabled",
+ default: false,
+ },
+ {
+ handler: _ => this.GeckoViewRemoteDebugger,
+ }
+ );
+
+ GeckoViewUtils.addLazyGetter(this, "DownloadTracker", {
+ module: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
+ ged: ["GeckoView:WebExtension:DownloadChanged"],
+ });
+
+ ChromeUtils.importESModule(
+ "resource://gre/modules/NotificationDB.sys.mjs"
+ );
+
+ // Listen for global EventDispatcher messages
+ lazy.EventDispatcher.instance.registerListener(this, [
+ "GeckoView:ResetUserPrefs",
+ "GeckoView:SetDefaultPrefs",
+ "GeckoView:SetLocale",
+ ]);
+
+ Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
+ Services.obs.addObserver(this, "handlersvc-store-initialized");
+
+ Services.obs.notifyObservers(null, "geckoview-startup-complete");
+ break;
+ }
+ case "browser-idle-startup-tasks-finished": {
+ // TODO bug 1730026: when an alternative is introduced that runs once,
+ // replace this observer topic with that alternative.
+ // This only needs to happen once during startup.
+ Services.obs.removeObserver(this, aTopic);
+ // Notify the start up crash tracker that the browser has successfully
+ // started up so the startup cache isn't rebuilt on next startup.
+ Services.startup.trackStartupCrashEnd();
+ break;
+ }
+ case "handlersvc-store-initialized": {
+ // Initialize PdfJs when running in-process and remote. This only
+ // happens once since PdfJs registers global hooks. If the PdfJs
+ // extension is installed the init method below will be overridden
+ // leaving initialization to the extension.
+ // parent only: configure default prefs, set up pref observers, register
+ // pdf content handler, and initializes parent side message manager
+ // shim for privileged api access.
+ try {
+ lazy.PdfJs.init(this._isNewProfile);
+ } catch {}
+ break;
+ }
+ }
+ }
+
+ onEvent(aEvent, aData, aCallback) {
+ debug`onEvent ${aEvent}`;
+
+ switch (aEvent) {
+ case "GeckoView:ResetUserPrefs": {
+ const prefs = new lazy.Preferences();
+ prefs.reset(aData.names);
+ break;
+ }
+ case "GeckoView:SetDefaultPrefs": {
+ const prefs = new lazy.Preferences({ defaultBranch: true });
+ for (const name of Object.keys(aData)) {
+ try {
+ prefs.set(name, aData[name]);
+ } catch (e) {
+ warn`Failed to set preference ${name}: ${e}`;
+ }
+ }
+ break;
+ }
+ case "GeckoView:SetLocale":
+ if (aData.requestedLocales) {
+ Services.locale.requestedLocales = aData.requestedLocales;
+ }
+ const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
+ Ci.nsIPrefLocalizedString
+ );
+ pls.data = aData.acceptLanguages;
+ Services.prefs.setComplexValue(
+ "intl.accept_languages",
+ Ci.nsIPrefLocalizedString,
+ pls
+ );
+ break;
+
+ case "GeckoView:StorageDelegate:Attached":
+ InitLater(() => {
+ const loginDetection = Cc[
+ "@mozilla.org/login-detection-service;1"
+ ].createInstance(Ci.nsILoginDetectionService);
+ loginDetection.init();
+ });
+ break;
+ }
+ }
+}
+
+GeckoViewStartup.prototype.classID = Components.ID(
+ "{8e993c34-fdd6-432c-967e-f995d888777f}"
+);
+GeckoViewStartup.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+]);
diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.cpp b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp
new file mode 100644
index 0000000000..71b9fadeb5
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp
@@ -0,0 +1,298 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#include "GeckoViewStreamListener.h"
+
+#include "mozilla/fallible.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIChannelEventSink.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpHeaderVisitor.h"
+#include "nsIInputStream.h"
+#include "nsINSSErrorsService.h"
+#include "nsITransportSecurityInfo.h"
+#include "nsIWebProgressListener.h"
+#include "nsIX509Cert.h"
+#include "nsPrintfCString.h"
+
+#include "nsNetUtil.h"
+
+#include "JavaBuiltins.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(GeckoViewStreamListener, nsIStreamListener,
+ nsIInterfaceRequestor, nsIChannelEventSink)
+
+class HeaderVisitor final : public nsIHttpHeaderVisitor {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ explicit HeaderVisitor(java::WebResponse::Builder::Param aBuilder)
+ : mBuilder(aBuilder) {}
+
+ NS_IMETHOD
+ VisitHeader(const nsACString& aHeader, const nsACString& aValue) override {
+ mBuilder->Header(aHeader, aValue);
+ return NS_OK;
+ }
+
+ private:
+ virtual ~HeaderVisitor() {}
+
+ const java::WebResponse::Builder::GlobalRef mBuilder;
+};
+
+NS_IMPL_ISUPPORTS(HeaderVisitor, nsIHttpHeaderVisitor)
+
+class StreamSupport final
+ : public java::GeckoInputStream::Support::Natives<StreamSupport> {
+ public:
+ typedef java::GeckoInputStream::Support::Natives<StreamSupport> Base;
+ using Base::AttachNative;
+ using Base::GetNative;
+
+ explicit StreamSupport(java::GeckoInputStream::Support::Param aInstance,
+ nsIRequest* aRequest)
+ : mInstance(aInstance), mRequest(aRequest) {}
+
+ void Close() {
+ mRequest->Cancel(NS_ERROR_ABORT);
+ mRequest->Resume();
+
+ // This is basically `delete this`, so don't run anything else!
+ Base::DisposeNative(mInstance);
+ }
+
+ void Resume() { mRequest->Resume(); }
+
+ private:
+ java::GeckoInputStream::Support::GlobalRef mInstance;
+ nsCOMPtr<nsIRequest> mRequest;
+};
+
+NS_IMETHODIMP
+GeckoViewStreamListener::OnStartRequest(nsIRequest* aRequest) {
+ MOZ_ASSERT(!mStream);
+
+ nsresult status;
+ aRequest->GetStatus(&status);
+ if (NS_FAILED(status)) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ CompleteWithError(status, channel);
+ return NS_OK;
+ }
+
+ // We're expecting data later via OnDataAvailable, so create the stream now.
+ InitializeStreamSupport(aRequest);
+
+ mStream = java::GeckoInputStream::New(mSupport);
+
+ // Suspend the request immediately. It will be resumed when (if) someone
+ // tries to read the Java stream.
+ aRequest->Suspend();
+
+ nsresult rv = HandleWebResponse(aRequest);
+ if (NS_FAILED(rv)) {
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ CompleteWithError(rv, channel);
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoViewStreamListener::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ if (mStream) {
+ if (NS_FAILED(aStatusCode)) {
+ mStream->SendError();
+ } else {
+ mStream->SendEof();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP GeckoViewStreamListener::OnDataAvailable(
+ nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset,
+ uint32_t aCount) {
+ MOZ_ASSERT(mStream);
+
+ // We only need this for the ReadSegments call, the value is unused.
+ uint32_t countRead;
+ nsresult rv =
+ aInputStream->ReadSegments(WriteSegment, this, aCount, &countRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+GeckoViewStreamListener::GetInterface(const nsIID& aIID, void** aResultOut) {
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
+ *aResultOut = static_cast<nsIChannelEventSink*>(this);
+ NS_ADDREF_THIS();
+ return NS_OK;
+ }
+
+ return NS_ERROR_NO_INTERFACE;
+}
+
+NS_IMETHODIMP
+GeckoViewStreamListener::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t flags,
+ nsIAsyncVerifyRedirectCallback* callback) {
+ callback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+/* static */
+nsresult GeckoViewStreamListener::WriteSegment(
+ nsIInputStream* aInputStream, void* aClosure, const char* aFromSegment,
+ uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) {
+ GeckoViewStreamListener* self =
+ static_cast<GeckoViewStreamListener*>(aClosure);
+ MOZ_ASSERT(self);
+ MOZ_ASSERT(self->mStream);
+
+ *aWriteCount = aCount;
+
+ jni::ByteArray::LocalRef buffer = jni::ByteArray::New(
+ reinterpret_cast<signed char*>(const_cast<char*>(aFromSegment)),
+ *aWriteCount, fallible);
+ if (!buffer) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (NS_FAILED(self->mStream->AppendBuffer(buffer))) {
+ // The stream was closed or something, abort reading this channel.
+ return NS_ERROR_ABORT;
+ }
+
+ return NS_OK;
+}
+
+nsresult GeckoViewStreamListener::HandleWebResponse(nsIRequest* aRequest) {
+ nsresult rv;
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // URI
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString uriSpec;
+ rv = uri->GetSpec(uriSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ java::WebResponse::Builder::LocalRef builder =
+ java::WebResponse::Builder::New(uriSpec);
+
+ // Body stream
+ if (mStream) {
+ builder->Body(mStream);
+ }
+
+ // Redirected
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ builder->Redirected(!loadInfo->RedirectChain().IsEmpty());
+
+ // Secure status
+ auto [certBytes, isSecure] = CertificateFromChannel(channel);
+ builder->IsSecure(isSecure);
+ if (certBytes) {
+ rv = builder->CertificateBytes(certBytes);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // We might need some additional info for response to http/https request
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel, &rv));
+ if (httpChannel) {
+ // Status code
+ uint32_t statusCode;
+ rv = httpChannel->GetResponseStatus(&statusCode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ builder->StatusCode(statusCode);
+
+ // Headers
+ RefPtr<HeaderVisitor> visitor = new HeaderVisitor(builder);
+ rv = httpChannel->VisitResponseHeaders(visitor);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Headers for other responses
+ // try to provide some basic metadata about the response
+ nsString filename;
+ if (NS_SUCCEEDED(channel->GetContentDispositionFilename(filename))) {
+ builder->Header(jni::StringParam(u"content-disposition"_ns),
+ nsPrintfCString("attachment; filename=\"%s\"",
+ NS_ConvertUTF16toUTF8(filename).get()));
+ }
+
+ nsCString contentType;
+ if (NS_SUCCEEDED(channel->GetContentType(contentType))) {
+ builder->Header(jni::StringParam(u"content-type"_ns), contentType);
+ }
+
+ int64_t contentLength = 0;
+ if (NS_SUCCEEDED(channel->GetContentLength(&contentLength))) {
+ nsString contentLengthString;
+ contentLengthString.AppendInt(contentLength);
+ builder->Header(jni::StringParam(u"content-length"_ns),
+ contentLengthString);
+ }
+ }
+
+ java::WebResponse::GlobalRef response = builder->Build();
+
+ SendWebResponse(response);
+ return NS_OK;
+}
+
+void GeckoViewStreamListener::InitializeStreamSupport(nsIRequest* aRequest) {
+ StreamSupport::Init();
+
+ mSupport = java::GeckoInputStream::Support::New();
+ StreamSupport::AttachNative(
+ mSupport, mozilla::MakeUnique<StreamSupport>(mSupport, aRequest));
+}
+
+std::tuple<jni::ByteArray::LocalRef, java::sdk::Boolean::LocalRef>
+GeckoViewStreamListener::CertificateFromChannel(nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsITransportSecurityInfo> securityInfo;
+ aChannel->GetSecurityInfo(getter_AddRefs(securityInfo));
+ if (!securityInfo) {
+ return std::make_tuple((jni::ByteArray::LocalRef) nullptr,
+ (java::sdk::Boolean::LocalRef) nullptr);
+ }
+
+ uint32_t securityState = 0;
+ securityInfo->GetSecurityState(&securityState);
+ auto isSecure = securityState == nsIWebProgressListener::STATE_IS_SECURE
+ ? java::sdk::Boolean::TRUE()
+ : java::sdk::Boolean::FALSE();
+
+ nsCOMPtr<nsIX509Cert> cert;
+ securityInfo->GetServerCert(getter_AddRefs(cert));
+ if (!cert) {
+ return std::make_tuple((jni::ByteArray::LocalRef) nullptr,
+ (java::sdk::Boolean::LocalRef) nullptr);
+ }
+
+ nsTArray<uint8_t> derBytes;
+ nsresult rv = cert->GetRawDER(derBytes);
+ NS_ENSURE_SUCCESS(rv,
+ std::make_tuple((jni::ByteArray::LocalRef) nullptr,
+ (java::sdk::Boolean::LocalRef) nullptr));
+
+ auto certBytes = jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(derBytes.Elements()), derBytes.Length());
+
+ return std::make_tuple(certBytes, isSecure);
+}
diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.h b/mobile/android/components/geckoview/GeckoViewStreamListener.h
new file mode 100644
index 0000000000..b42249f458
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewStreamListener.h
@@ -0,0 +1,57 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef GeckoViewStreamListener_h__
+#define GeckoViewStreamListener_h__
+
+#include "nsIStreamListener.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIChannelEventSink.h"
+
+#include "mozilla/widget/EventDispatcher.h"
+#include "mozilla/java/GeckoInputStreamNatives.h"
+#include "mozilla/java/WebResponseWrappers.h"
+
+#include "JavaBuiltins.h"
+
+namespace mozilla {
+
+class GeckoViewStreamListener : public nsIStreamListener,
+ public nsIInterfaceRequestor,
+ public nsIChannelEventSink {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+
+ explicit GeckoViewStreamListener() {}
+
+ static std::tuple<jni::ByteArray::LocalRef, java::sdk::Boolean::LocalRef>
+ CertificateFromChannel(nsIChannel* aChannel);
+
+ protected:
+ virtual ~GeckoViewStreamListener() {}
+
+ java::GeckoInputStream::GlobalRef mStream;
+ java::GeckoInputStream::Support::GlobalRef mSupport;
+
+ void InitializeStreamSupport(nsIRequest* aRequest);
+
+ static nsresult WriteSegment(nsIInputStream* aInputStream, void* aClosure,
+ const char* aFromSegment, uint32_t aToOffset,
+ uint32_t aCount, uint32_t* aWriteCount);
+
+ virtual nsresult HandleWebResponse(nsIRequest* aRequest);
+
+ virtual void SendWebResponse(java::WebResponse::Param aResponse) = 0;
+
+ virtual void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) = 0;
+};
+
+} // namespace mozilla
+
+#endif // GeckoViewStreamListener_h__
diff --git a/mobile/android/components/geckoview/LoginStorageDelegate.jsm b/mobile/android/components/geckoview/LoginStorageDelegate.jsm
new file mode 100644
index 0000000000..c5d3b118f4
--- /dev/null
+++ b/mobile/android/components/geckoview/LoginStorageDelegate.jsm
@@ -0,0 +1,138 @@
+/* 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 = ["LoginStorageDelegate"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm",
+ LoginEntry: "resource://gre/modules/GeckoViewAutocomplete.jsm",
+});
+
+const { debug, warn } = GeckoViewUtils.initLogging("LoginStorageDelegate");
+
+// Sync with LoginSaveOption.Hint in Autocomplete.java.
+const LoginStorageHint = {
+ NONE: 0,
+ GENERATED: 1 << 0,
+ LOW_CONFIDENCE: 1 << 1,
+};
+
+class LoginStorageDelegate {
+ _createMessage({ dismissed, autoSavedLoginGuid }, aLogins) {
+ let hint = LoginStorageHint.NONE;
+ if (dismissed) {
+ hint |= LoginStorageHint.LOW_CONFIDENCE;
+ }
+ if (autoSavedLoginGuid) {
+ hint |= LoginStorageHint.GENERATED;
+ }
+ return {
+ // Sync with PromptController
+ type: "Autocomplete:Save:Login",
+ hint,
+ logins: aLogins,
+ };
+ }
+
+ promptToSavePassword(
+ aBrowser,
+ aLogin,
+ dismissed = false,
+ notifySaved = false
+ ) {
+ const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal);
+ prompt.asyncShowPrompt(
+ this._createMessage({ dismissed }, [
+ lazy.LoginEntry.fromLoginInfo(aLogin),
+ ]),
+ result => {
+ const selectedLogin = result?.selection?.value;
+
+ if (!selectedLogin) {
+ return;
+ }
+
+ const loginInfo = lazy.LoginEntry.parse(selectedLogin).toLoginInfo();
+ Services.obs.notifyObservers(loginInfo, "passwordmgr-prompt-save");
+
+ lazy.GeckoViewAutocomplete.onLoginSave(selectedLogin);
+ }
+ );
+
+ return {
+ dismiss() {
+ prompt.dismiss();
+ },
+ };
+ }
+
+ promptToChangePassword(
+ aBrowser,
+ aOldLogin,
+ aNewLogin,
+ dismissed = false,
+ notifySaved = false,
+ autoSavedLoginGuid = ""
+ ) {
+ const newLogin = lazy.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 lazy.GeckoViewPrompter(aBrowser.ownerGlobal);
+ prompt.asyncShowPrompt(
+ this._createMessage({ dismissed, autoSavedLoginGuid }, [newLogin]),
+ result => {
+ const selectedLogin = result?.selection?.value;
+
+ if (!selectedLogin) {
+ return;
+ }
+
+ lazy.GeckoViewAutocomplete.onLoginSave(selectedLogin);
+
+ const loginInfo = lazy.LoginEntry.parse(selectedLogin).toLoginInfo();
+ Services.obs.notifyObservers(
+ loginInfo,
+ "passwordmgr-prompt-change",
+ oldGuid
+ );
+ }
+ );
+
+ return {
+ dismiss() {
+ prompt.dismiss();
+ },
+ };
+ }
+
+ promptToChangePasswordWithUsernames(aBrowser, aLogins, aNewLogin) {
+ this.promptToChangePassword(aBrowser, null /* oldLogin */, aNewLogin);
+ }
+}
+
+LoginStorageDelegate.prototype.classID = Components.ID(
+ "{3d765750-1c3d-11ea-aaef-0800200c9a66}"
+);
+LoginStorageDelegate.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsILoginManagerPrompter",
+]);
diff --git a/mobile/android/components/geckoview/PromptCollection.jsm b/mobile/android/components/geckoview/PromptCollection.jsm
new file mode 100644
index 0000000000..16ec60fd2e
--- /dev/null
+++ b/mobile/android/components/geckoview/PromptCollection.jsm
@@ -0,0 +1,48 @@
+/* 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 = ["PromptCollection"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
+});
+
+const { debug, warn } = GeckoViewUtils.initLogging("PromptCollection");
+
+class PromptCollection {
+ confirmRepost(browsingContext) {
+ const msg = {
+ type: "repost",
+ };
+ const prompter = new lazy.GeckoViewPrompter(browsingContext);
+ const result = prompter.showPrompt(msg);
+ return !!result?.allow;
+ }
+
+ asyncBeforeUnloadCheck(browsingContext) {
+ return new Promise(resolve => {
+ const msg = {
+ type: "beforeUnload",
+ };
+ const prompter = new lazy.GeckoViewPrompter(browsingContext);
+ prompter.asyncShowPrompt(msg, resolve);
+ }).then(result => !!result?.allow);
+ }
+
+ confirmFolderUpload() {
+ // Folder upload is not supported by GeckoView yet, see Bug 1674428.
+ return false;
+ }
+}
+
+PromptCollection.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIPromptCollection",
+]);
diff --git a/mobile/android/components/geckoview/ShareDelegate.jsm b/mobile/android/components/geckoview/ShareDelegate.jsm
new file mode 100644
index 0000000000..1e3a133953
--- /dev/null
+++ b/mobile/android/components/geckoview/ShareDelegate.jsm
@@ -0,0 +1,82 @@
+/* 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 = ["ShareDelegate"];
+
+const { GeckoViewUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/GeckoViewUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
+});
+
+const domBundle = Services.strings.createBundle(
+ "chrome://global/locale/dom/dom.properties"
+);
+
+const { debug, warn } = GeckoViewUtils.initLogging("ShareDelegate");
+
+class ShareDelegate {
+ init(aParent) {
+ this._openerWindow = aParent;
+ }
+
+ get openerWindow() {
+ return this._openerWindow;
+ }
+
+ async share(aTitle, aText, aUri) {
+ const ABORT = 2;
+ const FAILURE = 1;
+ const SUCCESS = 0;
+
+ const msg = {
+ type: "share",
+ title: aTitle,
+ text: aText,
+ uri: aUri ? aUri.displaySpec : null,
+ };
+ const prompt = new lazy.GeckoViewPrompter(this._openerWindow);
+ const result = await new Promise(resolve => {
+ prompt.asyncShowPrompt(msg, resolve);
+ });
+
+ if (!result) {
+ // 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) {
+ case FAILURE:
+ throw new DOMException(
+ domBundle.GetStringFromName("WebShareAPI_Failed"),
+ "DataError"
+ );
+ case ABORT: // Handle aborted attempt and invalid responses the same.
+ throw new DOMException(
+ domBundle.GetStringFromName("WebShareAPI_Aborted"),
+ "AbortError"
+ );
+ case SUCCESS:
+ return;
+ default:
+ throw new DOMException("Unknown error.", "UnknownError");
+ }
+ }
+}
+
+ShareDelegate.prototype.classID = Components.ID(
+ "{1201d357-8417-4926-a694-e6408fbedcf8}"
+);
+ShareDelegate.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsISharePicker",
+]);
diff --git a/mobile/android/components/geckoview/components.conf b/mobile/android/components/geckoview/components.conf
new file mode 100644
index 0000000000..d33567bb74
--- /dev/null
+++ b/mobile/android/components/geckoview/components.conf
@@ -0,0 +1,93 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{3e30d2a0-9934-11ea-bb37-0242ac130002}',
+ 'contract_ids': ['@mozilla.org/embedcomp/prompt-collection;1'],
+ 'jsm': 'resource://gre/modules/PromptCollection.jsm',
+ 'constructor': 'PromptCollection',
+ },
+ {
+ 'js_name': 'prompt',
+ 'cid': '{076ac188-23c1-4390-aa08-7ef1f78ca5d9}',
+ 'contract_ids': [
+ '@mozilla.org/prompter;1',
+ ],
+ 'interfaces': ['nsIPromptService'],
+ 'jsm': 'resource://gre/modules/GeckoViewPrompt.jsm',
+ 'constructor': 'PromptFactory',
+ },
+ {
+ 'cid': '{8e993c34-fdd6-432c-967e-f995d888777f}',
+ 'contract_ids': ['@mozilla.org/geckoview/startup;1'],
+ 'jsm': 'resource://gre/modules/GeckoViewStartup.jsm',
+ 'constructor': 'GeckoViewStartup',
+ },
+ {
+ 'cid': '{42f3c238-e8e8-4015-9ca2-148723a8afcf}',
+ 'contract_ids': ['@mozilla.org/content-permission/prompt;1'],
+ 'jsm': 'resource://gre/modules/GeckoViewPermission.jsm',
+ 'constructor': 'GeckoViewPermission',
+ },
+ {
+ 'cid': '{a54d84d7-98a4-4fec-b664-e42e512ae9cc}',
+ 'contract_ids': ['@mozilla.org/push/Service;1'],
+ 'jsm': 'resource://gre/modules/GeckoViewPush.jsm',
+ 'constructor': 'PushService',
+ },
+ {
+ 'cid': '{fc4bec74-ddd0-4ea8-9a66-9a5081258e32}',
+ 'contract_ids': ['@mozilla.org/parent/colorpicker;1'],
+ 'jsm': 'resource://gre/modules/ColorPickerDelegate.jsm',
+ 'constructor': 'ColorPickerDelegate',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{25fdbae6-f684-4bf0-b773-ff2b7a6273c8}',
+ 'contract_ids': ['@mozilla.org/parent/filepicker;1'],
+ 'jsm': 'resource://gre/modules/FilePickerDelegate.jsm',
+ 'constructor': 'FilePickerDelegate',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{1201d357-8417-4926-a694-e6408fbedcf8}',
+ 'contract_ids': ['@mozilla.org/sharepicker;1'],
+ 'jsm': 'resource://gre/modules/ShareDelegate.jsm',
+ 'constructor': 'ShareDelegate',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{3d765750-1c3d-11ea-aaef-0800200c9a66}',
+ 'contract_ids': ['@mozilla.org/login-manager/prompter;1'],
+ 'jsm': 'resource://gre/modules/LoginStorageDelegate.jsm',
+ 'constructor': 'LoginStorageDelegate',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{91455c77-64a1-4c37-be00-f94eb9c7b8e1}',
+ 'contract_ids': [
+ '@mozilla.org/uriloader/external-helper-app-service;1',
+ ],
+ 'type': 'GeckoViewExternalAppService',
+ 'constructor': 'GeckoViewExternalAppService::GetSingleton',
+ 'headers': ['GeckoViewExternalAppService.h'],
+ 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS,
+ },
+]
+
+if defined('MOZ_ANDROID_HISTORY'):
+ Classes += [
+ {
+ 'name': 'History',
+ 'cid': '{0937a705-91a6-417a-8292-b22eb10da86c}',
+ 'contract_ids': ['@mozilla.org/browser/history;1'],
+ 'singleton': True,
+ 'type': 'GeckoViewHistory',
+ 'headers': ['GeckoViewHistory.h'],
+ 'constructor': 'GeckoViewHistory::GetSingleton',
+ },
+ ]
diff --git a/mobile/android/components/geckoview/moz.build b/mobile/android/components/geckoview/moz.build
new file mode 100644
index 0000000000..607b02f367
--- /dev/null
+++ b/mobile/android/components/geckoview/moz.build
@@ -0,0 +1,49 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+SOURCES += [
+ "GeckoViewExternalAppService.cpp",
+ "GeckoViewOutputStream.cpp",
+ "GeckoViewStreamListener.cpp",
+]
+
+EXPORTS += [
+ "GeckoViewExternalAppService.h",
+ "GeckoViewOutputStream.h",
+ "GeckoViewStreamListener.h",
+]
+
+if CONFIG["MOZ_ANDROID_HISTORY"]:
+ EXPORTS += [
+ "GeckoViewHistory.h",
+ ]
+ SOURCES += [
+ "GeckoViewHistory.cpp",
+ ]
+ include("/ipc/chromium/chromium-config.mozbuild")
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXTRA_COMPONENTS += [
+ "GeckoView.manifest",
+]
+
+EXTRA_JS_MODULES += [
+ "ColorPickerDelegate.jsm",
+ "FilePickerDelegate.jsm",
+ "GeckoViewPermission.jsm",
+ "GeckoViewPrompt.jsm",
+ "GeckoViewPrompter.sys.mjs",
+ "GeckoViewPush.jsm",
+ "GeckoViewStartup.jsm",
+ "LoginStorageDelegate.jsm",
+ "PromptCollection.jsm",
+ "ShareDelegate.jsm",
+]
+
+FINAL_LIBRARY = "xul"