summaryrefslogtreecommitdiffstats
path: root/mobile/android/components/geckoview/GeckoViewHistory.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--mobile/android/components/geckoview/GeckoViewHistory.cpp508
1 files changed, 508 insertions, 0 deletions
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);
+ }
+}