blob: d914637b77c609d71bc83fe8a4f3cf8d6a8659b6 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/view_transition_commit_deferring_condition.h"
#include "base/memory/ptr_util.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/view_transition_opt_in_state.h"
#include "content/public/common/content_features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/frame/view_transition_state.h"
namespace content {
// static
std::unique_ptr<CommitDeferringCondition>
ViewTransitionCommitDeferringCondition::MaybeCreate(
NavigationRequest& navigation_request) {
// If we already have a transition animation, we should skip the view
// transition.
if (navigation_request.was_initiated_by_animated_transition()) {
return nullptr;
}
switch (navigation_request.frame_tree_node()->frame_tree().type()) {
case FrameTree::Type::kPrerender:
// Pre-rendered frame trees don't render any frames until activation. It's
// not feasible to run transitions in a frame which has no lifecycle
// updates.
return nullptr;
case FrameTree::Type::kPrimary:
break;
case FrameTree::Type::kFencedFrame:
// TODO(khushalsagar): Enable for fenced frames with a WPT.
return nullptr;
case FrameTree::Type::kGuest:
// TODO(crbug.com/40202416): Enable for MPArch based guests.
return nullptr;
};
RenderFrameHostImpl* old_rfh =
navigation_request.frame_tree_node()->current_frame_host();
if (!navigation_request.IsInMainFrame()) {
// We will not have a RFH if this navigation is not committing a new
// Document.
auto* new_rfh = navigation_request.GetRenderFrameHost();
if (!new_rfh) {
return nullptr;
}
// We don't have paint holding support for navigations from in-process to
// out-of-process iframes (or vice versa). Since ViewTransition requires
// paint holding, disable them for such navigations. This should be
// extremely rare for same-origin navigations.
if (old_rfh->is_local_root() != new_rfh->is_local_root()) {
return nullptr;
}
}
if (!navigation_request.ShouldDispatchPageSwapEvent()) {
return nullptr;
}
if (ViewTransitionOptInState::GetOrCreateForCurrentDocument(old_rfh)
->same_origin_opt_in() ==
blink::mojom::ViewTransitionSameOriginOptIn::kDisabled) {
return nullptr;
}
if (navigation_request.did_encounter_cross_origin_redirect()) {
return nullptr;
}
const url::Origin& current_request_origin = old_rfh->GetLastCommittedOrigin();
const url::Origin& new_request_origin =
navigation_request.is_running_potential_prerender_activation_checks()
? navigation_request.GetTentativeOriginAtRequestTime()
: *navigation_request.GetOriginToCommit();
if (current_request_origin != new_request_origin) {
return nullptr;
}
// https://drafts.csswg.org/css-view-transitions-2/#valdef-view-transition-navigation-auto
// `auto` is currently the only value and corresponds to enabling the boolean
// opt in.
switch (navigation_request.common_params().navigation_type) {
case blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT:
// Note: RESTORE is used for history traversals after a session restore so
// treat these as history traversal. The initial restore itself has no
// outgoing page so won't reach here.
case blink::mojom::NavigationType::RESTORE:
case blink::mojom::NavigationType::RESTORE_WITH_POST:
break;
case blink::mojom::NavigationType::DIFFERENT_DOCUMENT:
if (navigation_request.browser_initiated()) {
return nullptr;
}
break;
case blink::mojom::NavigationType::RELOAD:
case blink::mojom::NavigationType::RELOAD_BYPASSING_CACHE:
return nullptr;
case blink::mojom::NavigationType::SAME_DOCUMENT:
case blink::mojom::NavigationType::HISTORY_SAME_DOCUMENT:
// Same document navigations should already be excluded by
// `ShouldDispatchPageSwapEvent`.
NOTREACHED();
}
return base::WrapUnique(
new ViewTransitionCommitDeferringCondition(navigation_request));
}
ViewTransitionCommitDeferringCondition::ViewTransitionCommitDeferringCondition(
NavigationRequest& navigation_request)
: CommitDeferringCondition(navigation_request), weak_factory_(this) {}
ViewTransitionCommitDeferringCondition::
~ViewTransitionCommitDeferringCondition() {
// If we cached a view transition for the old Document and the navigation
// has been aborted, inform the old Document to discard the pending
// ViewTransition.
//
// Note: If we don't have `resources_`, they have been transferred to the
// NavigationRequest which is now responsible to discard the old transition if
// the navigation is cancelled.
if (!resources_ || !old_rfh_ || !old_rfh_->IsRenderFrameLive()) {
return;
}
old_rfh_->GetAssociatedLocalFrame()
->NotifyViewTransitionAbortedToOldDocument();
}
CommitDeferringCondition::Result
ViewTransitionCommitDeferringCondition::WillCommitNavigation(
base::OnceClosure resume) {
auto* navigation_request = NavigationRequest::From(&GetNavigationHandle());
auto* render_frame_host =
navigation_request->frame_tree_node()->current_frame_host();
blink::mojom::PageSwapEventParamsPtr page_swap_event_params =
navigation_request->WillDispatchPageSwap();
CHECK(page_swap_event_params);
blink::ViewTransitionToken transition_token;
resources_ =
std::make_unique<ScopedViewTransitionResources>(transition_token);
resume_navigation_ = std::move(resume);
old_rfh_ = render_frame_host->GetWeakPtr();
CHECK(render_frame_host->IsRenderFrameLive());
// Request a snapshot. This includes running any associaged script in the
// renderer process.
render_frame_host->GetAssociatedLocalFrame()
->SnapshotDocumentForViewTransition(
transition_token, std::move(page_swap_event_params),
base::BindOnce(&ViewTransitionCommitDeferringCondition::
OnSnapshotAckFromRenderer,
weak_factory_.GetWeakPtr()));
// Also post a timeout task to resume even if the renderer has not acked.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ViewTransitionCommitDeferringCondition::OnSnapshotTimeout,
weak_factory_.GetWeakPtr()),
GetSnapshotCallbackTimeout());
return Result::kDefer;
}
const char* ViewTransitionCommitDeferringCondition::TraceEventName() const {
return "ViewTransitionCommitDeferringCondition";
}
void ViewTransitionCommitDeferringCondition::OnSnapshotTimeout() {
if (resume_navigation_) {
std::move(resume_navigation_).Run();
}
}
base::TimeDelta
ViewTransitionCommitDeferringCondition::GetSnapshotCallbackTimeout() const {
// TODO(vmpstr): Figure out if we need to increase this in tests.
return base::Seconds(4);
}
void ViewTransitionCommitDeferringCondition::OnSnapshotAckFromRenderer(
const blink::ViewTransitionState& view_transition_state) {
// The timeout may have been triggered already.
if (!resume_navigation_) {
return;
}
base::ScopedClosureRunner runner(std::move(resume_navigation_));
if (view_transition_state.HasSubframeSnapshot()) {
if (!old_rfh_) {
return;
}
// The subframe snapshot is only used for in-process iframes which don't own
// a widget.
if (old_rfh_->is_local_root()) {
return;
}
auto* new_rfh =
NavigationRequest::From(&GetNavigationHandle())->GetRenderFrameHost();
// We shouldn't send a snapshot request unless the new RFH is also an
// in-process subframe.
CHECK(!new_rfh->is_local_root());
CHECK(!old_rfh_->is_main_frame()) << "Main frames must be local roots";
CHECK(!new_rfh->is_main_frame()) << "Main frames must be local roots";
CHECK_EQ(old_rfh_->GetProcess(), new_rfh->GetProcess())
<< "Navigation between 2 non-local roots must be in the ancestor "
"frame's process";
}
if (view_transition_state.IsValid()) {
NavigationRequest::From(&GetNavigationHandle())
->SetViewTransitionState(std::move(resources_),
std::move(view_transition_state));
}
}
} // namespace content