diff options
Diffstat (limited to 'mobile/android/components/geckoview/GeckoViewHistory.cpp')
-rw-r--r-- | mobile/android/components/geckoview/GeckoViewHistory.cpp | 508 |
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); + } +} |