chromium/content/browser/navigation_transitions/back_forward_transition_animator.cc

// Copyright 2024 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/navigation_transitions/back_forward_transition_animator.h"

#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "cc/slim/layer.h"
#include "cc/slim/solid_color_layer.h"
#include "cc/slim/surface_layer.h"
#include "cc/slim/ui_resource_layer.h"
#include "content/browser/navigation_transitions/back_forward_transition_animation_manager_android.h"
#include "content/browser/navigation_transitions/progress_bar.h"
#include "content/browser/renderer_host/compositor_impl_android.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot.h"
#include "content/browser/renderer_host/navigation_transitions/navigation_entry_screenshot_cache.h"
#include "content/browser/renderer_host/navigation_transitions/navigation_transition_config.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_android.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/web_contents/web_contents_view_android.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents_delegate.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "ui/android/window_android.h"
#include "ui/display/screen.h"
#include "ui/events/back_gesture_event.h"

namespace content {

namespace {

using CacheHitOrMissReason = NavigationTransitionData::CacheHitOrMissReason;

using NavigationDirection =
    BackForwardTransitionAnimationManager::NavigationDirection;
using AnimationStage = BackForwardTransitionAnimationManager::AnimationStage;
using SwitchSpringReason = PhysicsModel::SwitchSpringReason;
using SwipeEdge = ui::BackGestureEventSwipeEdge;

static constexpr base::TimeDelta kDismissScreenshotAfter = base::Seconds(4);

void ResetTransformForLayer(cc::slim::Layer* layer) {
  CHECK(layer);
  auto transform = layer->transform();
  transform.MakeIdentity();
  layer->SetTransform(transform);
}

bool ShouldUseFallbackScreenshot(
    BackForwardTransitionAnimationManagerAndroid* animation_manager,
    NavigationEntryImpl* destination_entry) {
  bool use_fallback_screenshot = true;
  auto* screenshot = static_cast<NavigationEntryScreenshot*>(
      destination_entry->GetUserData(NavigationEntryScreenshot::kUserDataKey));
  auto cache_hit_or_miss_reason =
      destination_entry->navigation_transition_data()
          .cache_hit_or_miss_reason();

  if (screenshot) {
    gfx::Size screenshot_size = screenshot->dimensions_without_compression();
    gfx::Size screen_size = animation_manager->web_contents_view_android()
                                ->GetNativeView()
                                ->GetPhysicalBackingSize();
    use_fallback_screenshot = screenshot_size != screen_size;
    if (screenshot_size != screen_size) {
      cache_hit_or_miss_reason = NavigationTransitionData::
          CacheHitOrMissReason::kCacheMissScreenshotOrientation;
    } else {
      CHECK_EQ(cache_hit_or_miss_reason.value(),
               NavigationTransitionData::CacheHitOrMissReason::kCacheHit);
    }
  }

  // TODO(crbug.com/355454946): Consider other ways to capture `kCacheColdStart`
  // metric.
  UMA_HISTOGRAM_ENUMERATION("Navigation.GestureTransition.CacheHitOrMissReason",
                            cache_hit_or_miss_reason.value_or(
                                CacheHitOrMissReason::kCacheMissColdStart));

  return use_fallback_screenshot;
}

//========================== Fitted animation timeline =========================
//
// The animations for `OnGestureProgressed` are driven purely by user gestures.
// We use `gfx::KeyframeEffect` for progressing the animation in response by
// setting up a fitted animation timeline (one second) and mapping gesture
// progress to the corresponding time value.
//
// The timeline for the scrim animation is also a function of layer's position.
// We also use this fitted timeline for scrim.
//
// Note: The timing function is linear.

static constexpr base::TimeTicks kFittedStart;
static constexpr base::TimeDelta kFittedTimelineDuration = base::Seconds(1);

base::TimeTicks GetFittedTimeTicksForForegroundProgress(float progress) {
  return kFittedStart + kFittedTimelineDuration * progress;
}

// 0-indexed as the value will be stored in a bitset.
enum class TargetProperty {
  kScrim = 0,
  kCrossFade,
  kFaviconOpacity,
  kFaviconPosition,
};

template <typename KeyFrameType>
struct KeyFrame {
  base::TimeDelta time;
  KeyFrameType value;
};

// Each `KeyFrame` is interpolated using a linear function.
template <typename KeyFrameType, std::size_t Size>
struct LinearModelConfig {
  TargetProperty target_property;
  std::array<KeyFrame<KeyFrameType>, Size> key_frames;
};

//============================= Crossfade animation ============================
static constexpr base::TimeDelta kCrossfadeDuration = base::Milliseconds(100);

static constexpr LinearModelConfig<float, 2u> kCrossFadeAnimation{
    .target_property = TargetProperty::kCrossFade,
    .key_frames = {KeyFrame{
                       .time = base::TimeDelta(),
                       .value = 1.0f,
                   },
                   KeyFrame{
                       .time = kCrossfadeDuration,
                       .value = 0.0f,
                   }}};

//=============================== Scrim animation ==============================
// The scrim range is from 0.2 to 0 in dark mode and 0.1 to 0 in light mode. The
// scrim value is a linear function of the top layer's position.
static constexpr LinearModelConfig<float, 2u> kScrimAnimationLightMode{
    .target_property = TargetProperty::kScrim,
    .key_frames = {KeyFrame{
                       .time = base::TimeDelta(),
                       .value = 0.1f,
                   },
                   KeyFrame{
                       .time = kFittedTimelineDuration,
                       .value = 0.0f,
                   }}};

static constexpr LinearModelConfig<float, 2u> kScrimAnimationDarkMode{
    .target_property = TargetProperty::kScrim,
    .key_frames = {KeyFrame{
                       .time = base::TimeDelta(),
                       .value = 0.2f,
                   },
                   KeyFrame{
                       .time = kFittedTimelineDuration,
                       .value = 0.0f,
                   }}};

template <typename KeyFrameType, std::size_t Size>
void AddLinearModelToEffect(
    LinearModelConfig<KeyFrameType, Size> config,
    std::conditional_t<std::is_same<KeyFrameType, float>::value,
                       gfx::FloatAnimationCurve::Target,
                       gfx::TransformAnimationCurve::Target>* target,
    gfx::KeyframeEffect& effect) {
  using CurveType = std::conditional_t<std::is_same<KeyFrameType, float>::value,
                                       gfx::KeyframedFloatAnimationCurve,
                                       gfx::KeyframedTransformAnimationCurve>;
  using KeyframeType =
      std::conditional_t<std::is_same<KeyFrameType, float>::value,
                         gfx::FloatKeyframe, gfx::TransformKeyframe>;

  auto curve = CurveType::Create();
  for (size_t i = 0; i < Size; ++i) {
    const auto& keyframe = config.key_frames.at(i);
    curve->AddKeyframe(KeyframeType::Create(/*time=*/keyframe.time,
                                            /*value=*/keyframe.value,
                                            /*timing_function=*/nullptr));
  }
  curve->set_target(target);
  auto model = gfx::KeyframeModel::Create(
      /*curve=*/std::move(curve),
      /*keyframe_model_id=*/effect.GetNextKeyframeModelId(),
      /*target_property_id=*/
      static_cast<int>(config.target_property));
  effect.AddKeyframeModel(std::move(model));
}

//================================ Fallback UX =================================
//
// Size of the favicon's rounded rectangle background.
constexpr static int kRRectSizeDip = 56;
// Radius of the rounded rectangle.
constexpr static float kRRectRadiusDip = 20.f;
// Relative position of the favicon with respect to the rounded rectangle.
constexpr static int kFaviconPosDip = 16;

static constexpr LinearModelConfig<float, 4u> kRRectOpacityModel{
    .target_property = TargetProperty::kFaviconOpacity,
    // The opacity is 0.f until 25% progress, and reaches 1.f at 50% progress.
    .key_frames = {
        KeyFrame{
            .time = base::TimeDelta(),
            .value = 0.f,
        },
        KeyFrame{
            .time = kFittedTimelineDuration * 0.25,
            .value = 0.0f,
        },
        KeyFrame{
            .time = kFittedTimelineDuration * 0.5,
            .value = 1.f,
        },
        KeyFrame{
            .time = kFittedTimelineDuration,
            .value = 1.f,
        },
    }};

scoped_refptr<cc::slim::SolidColorLayer> AddRoundedRectangle(
    cc::slim::Layer* parent,
    int size_px,
    float corner_radius_px,
    SkColor4f color) {
  auto rrect = cc::slim::SolidColorLayer::Create();
  // The motion of the fallback UX is driven by the `effect_`. The first ever
  // `OnGestureProgressed()` call at the end will move the rrect to its desired
  // starting position.
  rrect->SetPosition(gfx::PointF(0.f, 0.f));
  rrect->SetBounds(gfx::Size(size_px, size_px));
  rrect->SetRoundedCorner(gfx::RoundedCornersF(
      corner_radius_px, corner_radius_px, corner_radius_px, corner_radius_px));
  rrect->SetBackgroundColor(color);
  rrect->SetIsDrawable(true);
  parent->AddChild(rrect);
  return rrect;
}

}  // namespace

std::unique_ptr<BackForwardTransitionAnimator>
BackForwardTransitionAnimator::Factory::Create(
    WebContentsViewAndroid* web_contents_view_android,
    NavigationControllerImpl* controller,
    const ui::BackGestureEvent& gesture,
    NavigationDirection nav_direction,
    SwipeEdge initiating_edge,
    NavigationEntryImpl* destination_entry,
    SkBitmap embedder_content,
    BackForwardTransitionAnimationManagerAndroid* animation_manager) {
  return base::WrapUnique(new BackForwardTransitionAnimator(
      web_contents_view_android, controller, gesture, nav_direction,
      initiating_edge, destination_entry, std::move(embedder_content),
      animation_manager));
}

BackForwardTransitionAnimator::~BackForwardTransitionAnimator() {
  CHECK(IsTerminalState()) << ToString(state_);

  ResetTransformForLayer(animation_manager_->web_contents_view_android()
                             ->parent_for_web_page_widgets());

  // TODO(crbug.com/40283503): If there is the old visual state hovering
  // above the RWHV layer, we need to remove that as well.

  if (screenshot_layer_) {
    screenshot_scrim_->RemoveFromParent();
    screenshot_scrim_.reset();

    screenshot_layer_->RemoveFromParent();
    screenshot_layer_.reset();
  }

  ResetLiveOverlayLayer();

  if (!fallback_ux_) {
    CHECK_NE(ui_resource_id_, cc::UIResourceClient::kUninitializedUIResourceId);
    DeleteUIResource(ui_resource_id_);

    if (navigation_state_ != NavigationState::kCommitted) {
      CHECK(screenshot_);
      animation_manager_->navigation_controller()
          ->GetNavigationEntryScreenshotCache()
          ->SetScreenshot(nullptr, std::move(screenshot_),
                          is_copied_from_embedder_);
    } else {
      // If the navigation has committed then the destination entry is active.
      // We don't persist the screenshot for the active entry.
    }
  }

  // This can happen if the navigation started for this gesture was committed
  // but another navigation or gesture started before the destination renderer
  // produced its first frame.
  if (new_render_widget_host_) {
    CHECK_EQ(state_, State::kAnimationAborted) << ToString(state_);
    UnregisterNewFrameActivationObserver();
  }
}

// protected.
BackForwardTransitionAnimator::BackForwardTransitionAnimator(
    WebContentsViewAndroid* web_contents_view_android,
    NavigationControllerImpl* controller,
    const ui::BackGestureEvent& gesture,
    NavigationDirection nav_direction,
    SwipeEdge initiating_edge,
    NavigationEntryImpl* destination_entry,
    SkBitmap embedder_content,
    BackForwardTransitionAnimationManagerAndroid* animation_manager)
    : nav_direction_(nav_direction),
      initiating_edge_(initiating_edge),
      destination_entry_id_(destination_entry->GetUniqueID()),
      animation_manager_(animation_manager),
      is_copied_from_embedder_(destination_entry->navigation_transition_data()
                                   .is_copied_from_embedder()),
      device_scale_factor_(animation_manager_->web_contents_view_android()
                               ->GetTopLevelNativeWindow()
                               ->GetDipScale()),
      physics_model_(GetViewportWidthPx(),
                     web_contents_view_android->GetNativeView()->GetDipScale()),
      latest_progress_gesture_(gesture) {
  if (ShouldUseFallbackScreenshot(animation_manager_, destination_entry)) {
    fallback_ux_ = {
        .color_config = animation_manager_->web_contents_view_android()
                            ->web_contents()
                            ->GetDelegate()
                            ->GetBackForwardTransitionFallbackUXConfig(),
        .start_px = CalculateRRectStartPx(),
        .end_px = CalculateRRectEndPx(),
    };
  }
  state_ = State::kStarted;
  SetupForScreenshotPreview(std::move(embedder_content));
  ProcessState();
}

void BackForwardTransitionAnimator::OnGestureProgressed(
    const ui::BackGestureEvent& gesture) {
  CHECK_EQ(state_, State::kStarted);
  // `gesture.progress()` goes from 0.0 to 1.0 regardless of the edge being
  // swiped.
  CHECK_GE(gesture.progress(), 0.f);
  CHECK_LE(gesture.progress(), 1.f);
  // TODO(crbug.com/40287990): Should check the number of KeyFrameModels
  // is 1 (for scrim).

  float progress_delta =
      gesture.progress() - latest_progress_gesture_.progress();
  const float movement = progress_delta * GetViewportWidthPx();
  latest_progress_gesture_ = gesture;

  const PhysicsModel::Result result =
      physics_model_.OnGestureProgressed(movement, base::TimeTicks::Now());
  CHECK(!result.done);
  // The gesture animations are never considered "finished".
  bool animations_finished = SetLayerTransformationAndTickEffect(result);
  CHECK(!animations_finished);
}

void BackForwardTransitionAnimator::OnGestureCancelled() {
  CHECK_EQ(state_, State::kStarted);
  StartInputSuppression();
  AdvanceAndProcessState(State::kDisplayingCancelAnimation);
}

void BackForwardTransitionAnimator::OnGestureInvoked() {
  CHECK_EQ(state_, State::kStarted);

  StartInputSuppression();

  if (!StartNavigationAndTrackRequest()) {
    // `BackForwardTransitionAnimationManagerAndroid` will destroy `this` upon
    // return if the animation is aborted.
    if (state_ != State::kAnimationAborted) {
      AdvanceAndProcessState(State::kDisplayingCancelAnimation);
    }
    return;
  }

  // `StartNavigationAndTrackRequest()` sets `navigation_state_`.
  if (navigation_state_ == NavigationState::kBeforeUnloadDispatched) {
    AdvanceAndProcessState(State::kDisplayingCancelAnimation);
    return;
  }

  CHECK_EQ(navigation_state_, NavigationState::kStarted);
  AdvanceAndProcessState(State::kDisplayingInvokeAnimation);
}

void BackForwardTransitionAnimator::OnContentForNavigationEntryShown() {
  // Might be called multiple times if user swipes again before NTP fade
  // has finished.
  if (state_ != State::kWaitingForContentForNavigationEntryShown) {
    return;
  }
  // The embedder has finished cross-fading from the screenshot to the new
  // content. Unregister `this` from the `RenderWidgetHost` to stop the
  // `OnRenderWidgetHostDestroyed()` notification.
  CHECK(new_render_widget_host_);
  new_render_widget_host_->RemoveObserver(animation_manager_);
  new_render_widget_host_ = nullptr;
  AdvanceAndProcessState(State::kAnimationFinished);
}

AnimationStage BackForwardTransitionAnimator::GetCurrentAnimationStage() {
  switch (state_) {
    case State::kDisplayingInvokeAnimation:
      return AnimationStage::kInvokeAnimation;
    case State::kAnimationFinished:
    case State::kAnimationAborted:
      return AnimationStage::kNone;
    default:
      return AnimationStage::kOther;
  }
}

void BackForwardTransitionAnimator::OnAnimate(
    base::TimeTicks frame_begin_time) {
  bool animation_finished = false;

  switch (state_) {
    case State::kDisplayingCancelAnimation: {
      PhysicsModel::Result result = physics_model_.OnAnimate(frame_begin_time);
      std::ignore = SetLayerTransformationAndTickEffect(result);
      animation_finished = result.done;
      break;
    }
    case State::kDisplayingInvokeAnimation: {
      PhysicsModel::Result result = physics_model_.OnAnimate(frame_begin_time);
      animation_finished = SetLayerTransformationAndTickEffect(result);

      if (progress_bar_) {
        progress_bar_->Animate(frame_begin_time);
      }
      break;
    }
    case State::kDisplayingCrossFadeAnimation: {
      // The cross-fade model.
      CHECK_EQ(effect_.keyframe_models().size(), 1U);
      effect_.Tick(frame_begin_time);
      // `Tick()` has the side effect of removing all the finished models. At
      // the last frame of `OnFloatAnimated()`, the model is still running, but
      // is immediately removed after the `Tick()` WITHOUT advancing to the
      // finished or pending deletion state.
      animation_finished = effect_.keyframe_models().empty();
      break;
    }
    case State::kStarted:
    case State::kWaitingForBeforeUnloadResponse:
    case State::kWaitingForNewRendererToDraw:
    case State::kWaitingForContentForNavigationEntryShown:
    case State::kAnimationFinished:
    case State::kAnimationAborted:
      return;
  }

  if (animation_finished) {
    switch (state_) {
      case State::kDisplayingInvokeAnimation: {
        CHECK_EQ(navigation_state_, NavigationState::kCommitted);
        OnInvokeAnimationDisplayed();
        break;
      }
      case State::kDisplayingCancelAnimation: {
        OnCancelAnimationDisplayed();
        break;
      }
      case State::kDisplayingCrossFadeAnimation: {
        OnCrossFadeAnimationDisplayed();
        break;
      }
      case State::kStarted:
      case State::kWaitingForBeforeUnloadResponse:
      case State::kWaitingForNewRendererToDraw:
      case State::kWaitingForContentForNavigationEntryShown:
      case State::kAnimationFinished:
      case State::kAnimationAborted:
        NOTREACHED_IN_MIGRATION();
        break;
    }
  } else {
    animation_manager_->web_contents_view_android()
        ->GetTopLevelNativeWindow()
        ->SetNeedsAnimate();
  }
}

void BackForwardTransitionAnimator::OnRenderWidgetHostDestroyed(
    RenderWidgetHost* widget_host) {
  if (widget_host != new_render_widget_host_) {
    return;
  }
  // The subscribed `RenderWidgetHost` is getting destroyed. We must cancel the
  // transition and reset everything. This can happen for a client redirect,
  // where Viz never activates a frame from the committed renderer.
  CHECK_EQ(state_, State::kWaitingForNewRendererToDraw);
  CHECK_EQ(navigation_state_, NavigationState::kCommitted);
  AbortAnimation();
}

// This is only called after we subscribe to the new `RenderWidgetHost` when the
// navigation is ready to commit, meaning this method won't be called for
// 204/205/Download navigations, and won't be called if the navigation is
// cancelled.
void BackForwardTransitionAnimator::OnRenderFrameMetadataChangedAfterActivation(
    base::TimeTicks activation_time) {
  CHECK(tracked_request_);
  // We shouldn't get this notification for subframe navigations because we
  // never subscribe to the `RenderWidgetHost` for subframes.
  //
  // This is for simplicity: non-OOPIF / VideoSubmitter subframes share the same
  // `RenderWidgetHost` with the embedder thus it's difficult to differentiate
  // the frames submitted from a subframe vs from its embedder. For subframe
  // navigations, we play the cross-fade animation as soon as the invoke
  // animation has finished (see `DidFinishNavigation()`'s treatment for
  // subframes).
  CHECK(tracked_request_->is_primary_main_frame);

  // `new_render_widget_host_` and
  // `primary_main_frame_navigation_entry_item_sequence_number_` are set when
  // the navigation is ready to commit.
  CHECK(new_render_widget_host_);
  CHECK_NE(primary_main_frame_navigation_entry_item_sequence_number_,
           cc::RenderFrameMetadata::kInvalidItemSequenceNumber);

  // Viz can activate the frame before the DidCommit message arrives at the
  // browser (kStarted), since we start to get this notification when the
  // browser tells the renderer to commit the navigation.
  CHECK(navigation_state_ == NavigationState::kCommitted ||
        navigation_state_ == NavigationState::kStarted);

  // Again this notification is only received after the browser tells the
  // renderer to commit the navigation. So we must have started playing the
  // invoke animation, or the invoke animation has finished.
  CHECK(state_ == State::kDisplayingInvokeAnimation ||
        state_ == State::kWaitingForNewRendererToDraw)
      << ToString(state_);

  CHECK(!viz_has_activated_first_frame_)
      << "OnRenderFrameMetadataChangedAfterActivation can only be called once.";

  if (new_render_widget_host_->render_frame_metadata_provider()
          ->LastRenderFrameMetadata()
          .primary_main_frame_item_sequence_number !=
      primary_main_frame_navigation_entry_item_sequence_number_) {
    // We shouldn't dismiss the screenshot if the activated frame isn't what we
    // are expecting.
    return;
  }

  viz_has_activated_first_frame_ = true;

  // No longer interested in any other compositor frame submission
  // notifications. We can safely dismiss the previewed screenshot now.
  UnregisterNewFrameActivationObserver();

  if (state_ == State::kWaitingForNewRendererToDraw) {
    // Only display the crossfade animation if the old page is completely out of
    // the viewport.
    AdvanceAndProcessState(State::kDisplayingCrossFadeAnimation);
  }
}

// We only use `DidStartNavigation()` for signalling that the renderer has acked
// the BeforeUnload message to proceed (begin) the navigation.
void BackForwardTransitionAnimator::DidStartNavigation(
    NavigationHandle* navigation_handle) {
  // We need to set this state here since for same-document navigations, the
  // commit message is sent before the animator starts tracking the navigation.
  if (is_starting_navigation_) {
    NavigationRequest::From(navigation_handle)
        ->set_was_initiated_by_animated_transition();
  }

  if (!tracked_request_) {
    // We could reach here for an early-commit navigation:
    // - The animator only tracks the request's ID after `GoToIndex()` returns.
    // - In early commit, `DidStartNavigation()` is called during `GoToIndex()`.
    //
    // Early return here and let `StartNavigationAndTrackRequest()` to set the
    // `navigation_state_`.
    return;
  }

  if (tracked_request_->navigation_id != navigation_handle->GetNavigationId()) {
    return;
  }

  CHECK_EQ(navigation_state_, NavigationState::kBeforeUnloadDispatched);
  navigation_state_ = NavigationState::kBeforeUnloadAckedProceed;

  CHECK(state_ == State::kWaitingForBeforeUnloadResponse ||
        state_ == State::kDisplayingCancelAnimation);

  AdvanceAndProcessState(State::kDisplayingInvokeAnimation);
}

void BackForwardTransitionAnimator::ReadyToCommitNavigation(
    NavigationHandle* navigation_handle) {
  CHECK(!navigation_handle->IsSameDocument());

  if (!tracked_request_ ||
      tracked_request_->navigation_id != navigation_handle->GetNavigationId()) {
    // A unrelated navigation is ready to commit. This is possible with
    // NavigationQueuing. We ignore the unrelated navigation request.
    return;
  }

  if (!tracked_request_->is_primary_main_frame) {
    // We don't subscribe to the new widget host for subframes, nor clone the
    // old surface layer.
    return;
  }

  SubscribeToNewRenderWidgetHost(
      static_cast<NavigationRequest*>(navigation_handle));

  // Clone the Surface of the outgoing page for same-RFH navigations. We need to
  // this sooner for these navigations since the SurfaceID is updated when
  // sending the commit message.
  // For cross-RFH navigations, this is done as a part of processing the
  // DidCommit ack from the renderer.
  auto* navigation_request = NavigationRequest::From(navigation_handle);
  auto* old_rfh = RenderFrameHostImpl::FromID(
      navigation_request->GetPreviousRenderFrameHostId());
  auto* new_rfh = navigation_request->GetRenderFrameHost();

  // Ignore early swap cases for example crashed pages. They are same-RFH
  // navigations but the current SurfaceID of this RFH doesn't refer to content
  // from the old Document.
  if (navigation_request->early_render_frame_host_swap_type() ==
          NavigationRequest::EarlyRenderFrameHostSwapType::kNone &&
      old_rfh == new_rfh) {
    MaybeCloneOldSurfaceLayer(old_rfh->GetView());
  }
}

// - For a primary main frame navigation, we only use `DidFinishNavigation()`
// for navigations that never commit (204/205/downloads), or the cancelled /
// replaced navigations. For a committed navigation, everything is set in
// `OnDidNavigatePrimaryMainFramePreCommit()`, which is before the old
// `RenderViewHost` is swapped out.
//
// - For subframe navigation, we bring the fallback UX to the full viewport when
// the subframe navigation commits.
void BackForwardTransitionAnimator::DidFinishNavigation(
    NavigationHandle* navigation_handle) {
  // If we haven't started tracking a navigation, or if `navigation_handle`
  // isn't what we tracked, or if this `navigation_handle` has committed, ignore
  // it.
  //
  // TODO(https://crbug.com/357060513): If we are tracking a subframe request
  // from subframe A while subframe B navigates, the request in subframe B is
  // ignored completely. We should decide what to do before launch.
  if (!tracked_request_ ||
      tracked_request_->navigation_id != navigation_handle->GetNavigationId()) {
    return;
  }

  if (navigation_handle->HasCommitted()) {
    if (navigation_handle->IsInPrimaryMainFrame()) {
      // If this is a committed primary main frame navigation request, we must
      // have already set the states in
      // `OnDidNavigatePrimaryMainFramePreCommit()`.
      CHECK(tracked_request_->is_primary_main_frame);
      CHECK_EQ(navigation_state_, NavigationState::kCommitted);
    } else {
      // If this is a committed subframe request, animate the fallback UX to
      // occupy the full viewport.
      CHECK(!tracked_request_->is_primary_main_frame);
      navigation_state_ = NavigationState::kCommitted;
      physics_model_.OnNavigationFinished(/*navigation_committed=*/true);
      CHECK_EQ(state_, State::kDisplayingInvokeAnimation);
      // Signals that when the invoke animation finishes, play the cross-fade
      // animation directly.
      viz_has_activated_first_frame_ = true;
    }
    return;
  }

  CHECK_EQ(state_, State::kDisplayingInvokeAnimation);
  CHECK_EQ(navigation_state_, NavigationState::kStarted);
  navigation_state_ = NavigationState::kCancelled;
  physics_model_.OnNavigationFinished(/*navigation_committed=*/false);
  // 204/205/Download, or the ongoing navigation is cancelled. We need
  // to animate the old page back.
  //
  // TODO(crbug.com/41482488): We might need a better UX than
  // just display the cancel animation.
  AdvanceAndProcessState(State::kDisplayingCancelAnimation);
}

void BackForwardTransitionAnimator::OnDidNavigatePrimaryMainFramePreCommit(
    NavigationRequest* navigation_request,
    RenderFrameHostImpl* old_host,
    RenderFrameHostImpl* new_host) {
  // If a navigation commits in the primary main frame while we are tracking the
  // subframe requests, abort the animation immediately.
  if (tracked_request_ && !tracked_request_->is_primary_main_frame) {
    AbortAnimation();
    return;
  }

  CHECK(navigation_request->IsInPrimaryMainFrame());

  bool skip_all_animations = false;

  switch (state_) {
    case State::kStarted:
      CHECK(!tracked_request_);
      CHECK_EQ(navigation_state_, NavigationState::kNotStarted);
      // A new navigation finished in the primary main frame while the user is
      // swiping across the screen. For simplicity, destroy this class if the
      // new navigation was from the primary main frame.
      skip_all_animations = true;
      break;
    case State::kDisplayingInvokeAnimation: {
      // We can only get to `kDisplayingInvokeAnimation` if we have started
      // tracking the request.
      CHECK(tracked_request_);

      if (navigation_state_ == NavigationState::kStarted) {
        if (tracked_request_->navigation_id !=
            navigation_request->GetNavigationId()) {
          // A previously pending navigation has committed since we started
          // tracking our gesture navigation. Ignore this committed navigation.
          return;
        }

        // Before we display the crossfade animation to show the new page, we
        // need to check if the new page matches the origin of the screenshot.
        // We are not allowed to cross-fade from a screenshot of A.com to a page
        // of B.com.
        bool land_on_error_page = navigation_request->DidEncounterError();
        bool different_commit_origin = false;

        const auto& original_url = navigation_request->GetOriginalRequestURL();
        const auto& committed_url = navigation_request->GetURL();

        // The origin comparison is tricky because we do not know the precise
        // origin of the initial `NavigationRequest` (which depends on response
        // headers like CSP sandbox). It is reasonable to allow the animation to
        // proceed if the origins derived from the URL remains same-origin at
        // the end of the navigation, even if there is a sandboxing difference
        // that leads to an opaque origin. Also, URLs that can inherit origins
        // (e.g., about:blank) do not generally redirect, so it should be safe
        // to ignore inherited origins. Thus, we compare origins derived from
        // the URLs, after first checking whether the URL itself remains
        // unchanged (to account for URLs with opaque origins that won't appear
        // equal to each other, like data: URLs). This addresses concerns about
        // converting between URLs and origins (see
        // https://chromium.googlesource.com/chromium/src/+/main/docs/security/origin-vs-url.md).
        if (original_url != committed_url) {
          different_commit_origin =
              !url::Origin::Create(original_url)
                   .IsSameOriginWith(url::Origin::Create(committed_url));
        }

        if (!land_on_error_page && different_commit_origin) {
          skip_all_animations = true;
          break;
        }

        // Our gesture navigation has committed.
        navigation_state_ = NavigationState::kCommitted;
        physics_model_.OnNavigationFinished(/*navigation_committed=*/true);
        if (land_on_error_page) {
          // TODO(crbug.com/41482489): Implement a different UX if we
          // decide not show the animation at all (i.e. abort animation early
          // when we receive the response header).
        }
        // We need to check if hosts have changed, since they could have stayed
        // the same if the old page was early-swapped out, which can happen in
        // navigations from a crashed page.
        //
        // This is done sooner (in ReadyToCommit) for same-RFH navigations
        // since the SurfaceID changes before DidCommit for these navigations.
        if (old_host != new_host) {
          MaybeCloneOldSurfaceLayer(old_host->GetView());
        }
      } else {
        // Our navigation has already committed while a second navigation
        // commits. This can be a client redirect: A.com -> B.com and B.com's
        // document redirects to C.com, while we are still playing the post
        // commit-pending invoke animation to bring B.com's screenshot to the
        // center of the viewport.
        CHECK_EQ(navigation_state_, NavigationState::kCommitted);
        skip_all_animations = true;
      }
      break;
    }
    case State::kDisplayingCancelAnimation: {
      // We won't reach `NavigationState::kBeforeUnloadDispatched` because
      // if the request is blocked on BeforeUnload ack is cancelled, we will
      // receive `OnUnstartedNavigationCancelled()` where we advance
      // `navigation_state_` to `NavigationState::kCancelledBeforeStart`.

      CHECK(navigation_state_ == NavigationState::kNotStarted ||
            navigation_state_ == NavigationState::kBeforeUnloadDispatched ||
            navigation_state_ == NavigationState::kCancelled ||
            navigation_state_ == NavigationState::kCancelledBeforeStart)
          << ToString(navigation_state_);

      // A navigation finished while we are displaying the cancel animation.
      // For simplicity, destroy `this` and reset everything.
      skip_all_animations = true;
      break;
    }
    case State::kWaitingForNewRendererToDraw:
      // Our navigation has already committed while a second navigation commits.
      // This can be a client redirect: A.com -> B.com and B.com's document
      // redirects to C.com, before B.com's renderer even submits a new frame.
      CHECK_EQ(navigation_state_, NavigationState::kCommitted);
      CHECK(tracked_request_);
      skip_all_animations = true;
      break;
    case State::kWaitingForContentForNavigationEntryShown:
      // Our navigation has already committed while waiting for a native
      // entry to be finished drawing by the embedder.
      CHECK_EQ(navigation_state_, NavigationState::kCommitted);
      CHECK(tracked_request_);
      skip_all_animations = true;
      break;
    case State::kDisplayingCrossFadeAnimation: {
      // Our navigation has already committed while a second navigation commits.
      // This can be a client redirect: A.com -> B.com and B.com's document
      // redirects to C.com, while we are cross-fading from B.com's screenshot
      // to whatever is underneath the screenshot.
      CHECK_EQ(navigation_state_, NavigationState::kCommitted);
      CHECK(tracked_request_);
      skip_all_animations = true;
      break;
    }
    case State::kWaitingForBeforeUnloadResponse:
      skip_all_animations = true;
      break;
    case State::kAnimationFinished:
    case State::kAnimationAborted:
      NOTREACHED_IN_MIGRATION()
          << "No navigations can commit during the animator's destruction "
             "because the destruction is atomic.";
      break;
  }

  if (skip_all_animations) {
    AbortAnimation();
  }
}

// TODO(https://crbug.com/357094180): We should cancel the transition if a
// unrelated request shows a beforeunload dialog.
void BackForwardTransitionAnimator::OnNavigationCancelledBeforeStart(
    NavigationHandle* navigation_handle) {
  if (!tracked_request_ ||
      tracked_request_->navigation_id != navigation_handle->GetNavigationId()) {
    // A unrelated request is cancelled before start.
    return;
  }

  // For now only a BeforeUnload can defer the start of a navigation.
  //
  // NOTE: Even if the renderer acks the BeforeUnload message to proceed the
  // navigation, the navigation can still fail (see the early out in
  // BeginNavigationImpl()). However the animator's `navigation_state_` will
  // remain `NavigationState::kBeforeUnloadDispatched` because we only advance
  // from `NavigationState::kBeforeUnloadDispatched` to the next state at
  // `DidStartNavigation()`. In other words, if for any reason the navigation
  // fails after the renderer's ack, the below CHECK_EQ still holds.
  CHECK_EQ(navigation_state_, NavigationState::kBeforeUnloadDispatched);
  navigation_state_ = NavigationState::kCancelledBeforeStart;

  if (state_ == State::kWaitingForBeforeUnloadResponse) {
    // The cancel animation has already finished.
    AdvanceAndProcessState(State::kAnimationFinished);
  } else {
    // Let the cancel animation finish playing. We will advance to
    // `State::kAnimationFinished`.
    CHECK_EQ(state_, State::kDisplayingCancelAnimation);
  }
}

void BackForwardTransitionAnimator::AbortAnimation() {
  AdvanceAndProcessState(State::kAnimationAborted);
}

bool BackForwardTransitionAnimator::IsTerminalState() {
  return state_ == State::kAnimationFinished ||
         state_ == State::kAnimationAborted;
}

void BackForwardTransitionAnimator::OnFloatAnimated(
    const float& value,
    int target_property_id,
    gfx::KeyframeModel* keyframe_model) {
  TargetProperty property = static_cast<TargetProperty>(target_property_id);
  switch (property) {
    case TargetProperty::kScrim: {
      CHECK(screenshot_scrim_);
      auto scrim = SkColors::kBlack;
      scrim.fA = value;
      screenshot_scrim_->SetBackgroundColor(scrim);
      return;
    }
    case TargetProperty::kCrossFade: {
      CHECK(screenshot_layer_);
      screenshot_layer_->SetOpacity(value);
      return;
    }
    case TargetProperty::kFaviconOpacity: {
      CHECK(rounded_rectangle_);
      rounded_rectangle_->SetOpacity(value);
      return;
    }
    case TargetProperty::kFaviconPosition: {
      break;
    }
  }
  NOTREACHED_IN_MIGRATION();
}

void BackForwardTransitionAnimator::OnTransformAnimated(
    const gfx::TransformOperations& transform,
    int target_property_id,
    gfx::KeyframeModel* keyframe_model) {
  TargetProperty property = static_cast<TargetProperty>(target_property_id);
  switch (property) {
    case TargetProperty::kFaviconPosition: {
      CHECK(fallback_ux_);
      CHECK(rounded_rectangle_);
      rounded_rectangle_->SetTransform(transform.Apply());
      return;
    }
    case TargetProperty::kScrim:
    case TargetProperty::kCrossFade:
    case TargetProperty::kFaviconOpacity:
      break;
  }
  NOTREACHED_IN_MIGRATION();
}

void BackForwardTransitionAnimator::OnCancelAnimationDisplayed() {
  CHECK_EQ(effect_.keyframe_models()[0]->TargetProperty(),
           static_cast<int>(TargetProperty::kScrim));
  if (navigation_state_ == NavigationState::kBeforeUnloadDispatched) {
    AdvanceAndProcessState(State::kWaitingForBeforeUnloadResponse);
    return;
  }
  effect_.RemoveAllKeyframeModels();
  AdvanceAndProcessState(State::kAnimationFinished);
}

void BackForwardTransitionAnimator::OnInvokeAnimationDisplayed() {
  ResetLiveOverlayLayer();

  if (progress_bar_) {
    progress_bar_->GetLayer()->RemoveFromParent();
    progress_bar_.reset();
  }

  // The scrim timeline is a function of the top layer's position. At the end of
  // the invoke animation, the top layer is completely out of the viewport, so
  // the `KeyFrameModel` for the scrim should also be exhausted and removed.
  CHECK(effect_.keyframe_models().empty());
  if (is_copied_from_embedder_) {
    AdvanceAndProcessState(State::kWaitingForContentForNavigationEntryShown);
  } else if (viz_has_activated_first_frame_) {
    AdvanceAndProcessState(State::kDisplayingCrossFadeAnimation);
  } else {
    AdvanceAndProcessState(State::kWaitingForNewRendererToDraw);
  }
}

void BackForwardTransitionAnimator::OnCrossFadeAnimationDisplayed() {
  CHECK(effect_.keyframe_models().empty());
  AdvanceAndProcessState(State::kAnimationFinished);
}

// static.
bool BackForwardTransitionAnimator::CanAdvanceTo(State from, State to) {
  switch (from) {
    case State::kStarted:
      return to == State::kDisplayingCancelAnimation ||
             to == State::kDisplayingInvokeAnimation ||
             to == State::kAnimationAborted;
    case State::kWaitingForBeforeUnloadResponse:
      return to == State::kDisplayingInvokeAnimation ||
             to == State::kAnimationFinished || to == State::kAnimationAborted;
    case State::kDisplayingInvokeAnimation:
      return to == State::kDisplayingCrossFadeAnimation ||
             to == State::kWaitingForNewRendererToDraw ||
             // A second navigation replaces the current one, or the user hits
             // the stop button.
             to == State::kDisplayingCancelAnimation ||
             to == State::kWaitingForContentForNavigationEntryShown ||
             to == State::kAnimationAborted;
    case State::kWaitingForNewRendererToDraw:
      return to == State::kDisplayingCrossFadeAnimation ||
             to == State::kAnimationAborted;
    case State::kWaitingForContentForNavigationEntryShown:
      return to == State::kAnimationFinished || to == State::kAnimationAborted;
    case State::kDisplayingCrossFadeAnimation:
      return to == State::kAnimationFinished || to == State::kAnimationAborted;
    case State::kDisplayingCancelAnimation:
      return to == State::kAnimationFinished ||
             // The cancel animation has finished for a dispatched BeforeUnload
             // message.
             to == State::kWaitingForBeforeUnloadResponse ||
             // The renderer acks the BeforeUnload message to proceed the
             // navigation, BEFORE the cancel animation finishes.
             to == State::kDisplayingInvokeAnimation ||
             to == State::kAnimationAborted;
    case State::kAnimationFinished:
    case State::kAnimationAborted:
      NOTREACHED();
  }
}

// static.
std::string BackForwardTransitionAnimator::ToString(State state) {
  switch (state) {
    case State::kStarted:
      return "kStarted";
    case State::kDisplayingCancelAnimation:
      return "kDisplayingCancelAnimation";
    case State::kDisplayingInvokeAnimation:
      return "kDisplayingInvokeAnimation";
    case State::kWaitingForNewRendererToDraw:
      return "kWaitingForNewRendererToDraw";
    case State::kWaitingForContentForNavigationEntryShown:
      return "kWaitingForContentForNavigationEntryShown";
    case State::kDisplayingCrossFadeAnimation:
      return "kDisplayingCrossFadeAnimation";
    case State::kAnimationFinished:
      return "kAnimationFinished";
    case State::kWaitingForBeforeUnloadResponse:
      return "kWaitingForBeforeUnloadResponse";
    case State::kAnimationAborted:
      return "kAnimationAborted";
  }
  NOTREACHED();
}

// static.
std::string BackForwardTransitionAnimator::ToString(NavigationState state) {
  switch (state) {
    case NavigationState::kNotStarted:
      return "kNotStarted";
    case NavigationState::kBeforeUnloadDispatched:
      return "kBeforeUnloadDispatched";
    case NavigationState::kBeforeUnloadAckedProceed:
      return "kBeforeUnloadAckedProceed";
    case NavigationState::kCancelledBeforeStart:
      return "kCancelledBeforeStart";
    case NavigationState::kStarted:
      return "kStarted";
    case NavigationState::kCommitted:
      return "kCommitted";
    case NavigationState::kCancelled:
      return "kCancelled";
  }
  NOTREACHED();
}

void BackForwardTransitionAnimator::
    InitializeEffectForGestureProgressAnimation() {
  // The KeyFrameModel for scrim is added when we set up the screenshot layer,
  // at which we must have no models yet.
  CHECK(effect_.keyframe_models().empty());

  const blink::web_pref::WebPreferences& web_prefs =
      animation_manager_->web_contents_view_android()
          ->web_contents()
          ->GetOrCreateWebPreferences();

  if (web_prefs.preferred_color_scheme ==
      blink::mojom::PreferredColorScheme::kDark) {
    AddLinearModelToEffect(kScrimAnimationDarkMode, this, effect_);
  } else {
    AddLinearModelToEffect(kScrimAnimationLightMode, this, effect_);
  }
  if (rounded_rectangle_) {
    CHECK(fallback_ux_);
    AddLinearModelToEffect(kRRectOpacityModel, this, effect_);
    gfx::TransformOperations start;
    start.AppendTranslate(fallback_ux_->start_px.x(),
                          fallback_ux_->start_px.y(), 0.f);
    gfx::TransformOperations end;
    end.AppendTranslate(fallback_ux_->end_px.x(), fallback_ux_->end_px.y(),
                        0.f);
    AddLinearModelToEffect(
        LinearModelConfig<gfx::TransformOperations, 2u>{
            .target_property = TargetProperty::kFaviconPosition,
            .key_frames =
                {
                    KeyFrame{
                        .time = base::TimeDelta(),
                        .value = start,
                    },
                    KeyFrame{
                        .time = kFittedTimelineDuration,
                        .value = end,
                    },
                },
        },
        this, effect_);
  }
}

void BackForwardTransitionAnimator::InitializeEffectForCrossfadeAnimation() {
  // Before we add the cross-fade model, the scrim model must have finished.
  CHECK(effect_.keyframe_models().empty());

  AddLinearModelToEffect(kCrossFadeAnimation, this, effect_);
}

void BackForwardTransitionAnimator::AdvanceAndProcessState(State state) {
  CHECK(CanAdvanceTo(state_, state))
      << "Cannot advance from " << ToString(state_) << " to "
      << ToString(state);
  auto previous_animation_stage = GetCurrentAnimationStage();
  state_ = state;
  if (previous_animation_stage != GetCurrentAnimationStage()) {
    animation_manager_->OnAnimationStageChanged();
  }
  ProcessState();
}

void BackForwardTransitionAnimator::ProcessState() {
  switch (state_) {
    case State::kStarted: {
      break;
      // `this` will be waiting for the `OnGestureProgressed` call.
    }
    case State::kDisplayingCancelAnimation: {
      if (navigation_state_ == NavigationState::kNotStarted) {
        // When the user lifts the finger and signals not to start the
        // navigation.
        physics_model_.SwitchSpringForReason(
            SwitchSpringReason::kGestureCancelled);
      } else if (navigation_state_ ==
                 NavigationState::kBeforeUnloadDispatched) {
        // Notify the physics model we need to animate the active page back to
        // the center of the viewport because the browser has asked the renderer
        // to ack the BeforeUnload message. The renderer may need to show a
        // prompt to ask for the user input.
        physics_model_.SwitchSpringForReason(
            SwitchSpringReason::kBeforeUnloadDispatched);
      } else if (navigation_state_ == NavigationState::kCancelledBeforeStart) {
        // The user has interacted with the prompt to not start the navigation.
        // We are waiting for the ongoing cancel animation to finish.
      } else if (navigation_state_ == NavigationState::kCancelled) {
        // When the ongoing navigaion is cancelled because the user hits stop or
        // the navigation was replaced by another navigation,
        // `OnDidFinishNavigation()` has already notified the physics model to
        // switch to the cancel spring.
      } else {
        NOTREACHED_IN_MIGRATION() << ToString(navigation_state_);
      }
      CHECK(animation_manager_->web_contents_view_android()
                ->GetTopLevelNativeWindow());
      animation_manager_->web_contents_view_android()
          ->GetTopLevelNativeWindow()
          ->SetNeedsAnimate();
      break;
    }
    case State::kDisplayingInvokeAnimation: {
      if (navigation_state_ == NavigationState::kBeforeUnloadAckedProceed) {
        // Notify the physics model that the renderer has ack'ed BeforeUnload
        // and the navigation shall proceed.
        physics_model_.SwitchSpringForReason(
            SwitchSpringReason::kBeforeUnloadAckProceed);
        navigation_state_ = NavigationState::kStarted;
      } else {
        // Else, we must have started the navigation.
        CHECK_EQ(navigation_state_, NavigationState::kStarted);
        physics_model_.SwitchSpringForReason(
            SwitchSpringReason::kGestureInvoked);
      }
      CHECK(animation_manager_->web_contents_view_android()
                ->GetTopLevelNativeWindow());
      SetupProgressBar();
      animation_manager_->web_contents_view_android()
          ->GetTopLevelNativeWindow()
          ->SetNeedsAnimate();
      break;
    };
    case State::kWaitingForBeforeUnloadResponse: {
      // No-op. Waiting for the renderer's ack before we can proceed with the
      // navigation and animation or cancel everything.
      break;
    }
    case State::kWaitingForNewRendererToDraw: {
      dismiss_screenshot_timer_.Start(
          FROM_HERE, kDismissScreenshotAfter,
          base::BindOnce(
              &BackForwardTransitionAnimator::OnPostNavigationFirstFrameTimeout,
              weak_ptr_factory_.GetWeakPtr()));
      // No-op. Waiting for `OnRenderFrameMetadataChangedAfterActivation()`.
      break;
    }
    case State::kWaitingForContentForNavigationEntryShown:
      // No-op.
      break;
    case State::kDisplayingCrossFadeAnimation: {
      dismiss_screenshot_timer_.Stop();
      // Before we start displaying the crossfade animation,
      // `parent_for_web_page_widgets()` is completely out of the viewport. This
      // layer is reused for new content. For this reason, before we can start
      // the cross-fade we need to bring it back to the center of the viewport.
      ResetTransformForLayer(animation_manager_->web_contents_view_android()
                                 ->parent_for_web_page_widgets());
      ResetTransformForLayer(screenshot_layer_.get());

      // Move the screenshot to the very top, so we can cross-fade from the
      // screenshot (top) into the active page (bottom).
      InsertLayersInOrder();

      InitializeEffectForCrossfadeAnimation();

      CHECK(animation_manager_->web_contents_view_android()
                ->GetTopLevelNativeWindow());
      animation_manager_->web_contents_view_android()
          ->GetTopLevelNativeWindow()
          ->SetNeedsAnimate();
      break;
    }
    case State::kAnimationFinished:
    case State::kAnimationAborted:
      break;
  }
}

void BackForwardTransitionAnimator::SetupForScreenshotPreview(
    SkBitmap embedder_content) {
  NavigationControllerImpl* nav_controller =
      animation_manager_->navigation_controller();
  auto* destination_entry =
      nav_controller->GetEntryWithUniqueID(destination_entry_id_);
  CHECK(destination_entry);
  auto* preview = static_cast<NavigationEntryScreenshot*>(
      destination_entry->GetUserData(NavigationEntryScreenshot::kUserDataKey));
  CHECK(fallback_ux_ ||
        preview->navigation_entry_id() == destination_entry_id_);

  // The layers can be reused. We need to make sure there is no ongoing
  // transform on the layer of the current `WebContents`'s view.
  auto transform = animation_manager_->web_contents_view_android()
                       ->parent_for_web_page_widgets()
                       ->transform();
  CHECK(transform.IsIdentity()) << transform.ToString();

  if (fallback_ux_) {
    auto screenshot_layer = cc::slim::SolidColorLayer::Create();
    screenshot_layer->SetBackgroundColor(
        fallback_ux_->color_config.background_color);
    screenshot_layer_ = std::move(screenshot_layer);
  } else {
    auto* cache = nav_controller->GetNavigationEntryScreenshotCache();
    screenshot_ = cache->RemoveScreenshot(destination_entry);

    ui_resource_id_ = CreateUIResource(screenshot_.get());
    auto screenshot_layer = cc::slim::UIResourceLayer::Create();
    screenshot_layer->SetUIResourceId(ui_resource_id_);
    screenshot_layer_ = std::move(screenshot_layer);
  }
  screenshot_layer_->SetIsDrawable(true);
  screenshot_layer_->SetPosition(gfx::PointF(0.f, 0.f));
  screenshot_layer_->SetBounds(animation_manager_->web_contents_view_android()
                                   ->GetNativeView()
                                   ->GetPhysicalBackingSize());

  screenshot_scrim_ = cc::slim::SolidColorLayer::Create();
  screenshot_scrim_->SetBounds(screenshot_layer_->bounds());
  screenshot_scrim_->SetIsDrawable(true);
  screenshot_scrim_->SetBackgroundColor(SkColors::kTransparent);

  // Makes sure `screenshot_scrim_` is drawn on top of `screenshot_layer_`.
  screenshot_layer_->AddChild(screenshot_scrim_);
  screenshot_scrim_->SetContentsOpaque(false);

  // Add the rounded rectangle and the favicon. We need to do this after setting
  // up the scrim because the scrim shouldn't be applied to the rounded
  // rectangle and the favicon.
  const auto& favicon_bitmap =
      destination_entry->navigation_transition_data().favicon();
  // Do not draw the rrect if we don't have a valid bitmap.
  bool should_draw_rrect = fallback_ux_ && !favicon_bitmap.drawsNothing();
  if (should_draw_rrect) {
    auto favicon = cc::slim::UIResourceLayer::Create();
    auto favicon_width = favicon_bitmap.width();
    auto favicon_height = favicon_bitmap.height();
    favicon->SetBitmap(favicon_bitmap);
    favicon->SetIsDrawable(true);
    favicon->SetPosition(
        gfx::PointF(DipToPx(kFaviconPosDip), DipToPx(kFaviconPosDip)));
    favicon->SetBounds(gfx::Size(favicon_width, favicon_height));
    rounded_rectangle_ =
        AddRoundedRectangle(screenshot_layer_.get(), DipToPx(kRRectSizeDip),
                            DipToPx(kRRectRadiusDip),
                            fallback_ux_->color_config.rounded_rectangle_color);
    rounded_rectangle_->AddChild(std::move(favicon));
  }

  SetUpEmbedderContentLayerIfNeeded(std::move(embedder_content));

  // This inserts the screenshot layer into the layer tree.
  InsertLayersInOrder();

  // Set up `effect_`.
  InitializeEffectForGestureProgressAnimation();

  // Calling `OnGestureProgressed` manually. This will ask the physics model to
  // move the layers to their respective initial positions.
  OnGestureProgressed(latest_progress_gesture_);
}

void BackForwardTransitionAnimator::SetupProgressBar() {
  const auto& progress_bar_config =
      animation_manager_->web_contents_view_android()
          ->GetNativeView()
          ->GetWindowAndroid()
          ->GetProgressBarConfig();
  if (!progress_bar_config.ShouldDisplay()) {
    return;
  }

  progress_bar_ =
      std::make_unique<ProgressBar>(GetViewportWidthPx(), progress_bar_config);

  // The progress bar should draw on top of the scrim (if any).
  screenshot_layer_->AddChild(progress_bar_->GetLayer());
}

bool BackForwardTransitionAnimator::StartNavigationAndTrackRequest() {
  CHECK(fallback_ux_ || screenshot_);
  CHECK(!tracked_request_);
  CHECK_EQ(navigation_state_, NavigationState::kNotStarted);

  NavigationControllerImpl* nav_controller =
      animation_manager_->navigation_controller();

  int index = nav_controller->GetEntryIndexWithUniqueID(destination_entry_id_);
  if (index == -1) {
    return false;
  }

  std::vector<base::WeakPtr<NavigationRequest>> requests;
  {
    CHECK(!is_starting_navigation_);
    base::AutoReset reset(&is_starting_navigation_, true);
    requests = nav_controller->GoToIndexAndReturnAllRequests(index);
  }
  if (requests.empty()) {
    // The gesture did not create any navigation requests.
    return false;
  }

  for (const auto& request : requests) {
    request->set_was_initiated_by_animated_transition();
    if (request->IsInPrimaryMainFrame()) {
      TrackRequest(std::move(request));
      return true;
    }
  }

  if (requests.size() > 1U) {
    AbortAnimation();
    return false;
  }

  CHECK(!tracked_request_);
  CHECK_EQ(navigation_state_, NavigationState::kNotStarted);
  TrackRequest(std::move(requests[0]));
  return true;
}

void BackForwardTransitionAnimator::TrackRequest(
    base::WeakPtr<NavigationRequest> created_request) {
  CHECK(created_request);
  // The resulting `NavigationRequest` must be associated with the intended
  // `NavigationEntry`, to safely start the animation.
  //
  // NOTE: A `NavigationRequest` does not always have a `NavigationEntry`, since
  // the entry can be deleted at any time (e.g., clearing history), even during
  // a pending navigation. It's fine to CHECK the entry here because we just
  // created the requests in the same stack. No code yet had a chance to delete
  // the entry.
  CHECK(created_request->GetNavigationEntry());

  int request_entry_id = created_request->GetNavigationEntry()->GetUniqueID();

  // `destination_entry_id_` is initialized in the same stack as
  // `GoToIndexAndReturnAllRequests()`. Thus they must equal.
  CHECK_EQ(destination_entry_id_, request_entry_id);

  tracked_request_ = TrackedRequest{
      .navigation_id = created_request->GetNavigationId(),
      .is_primary_main_frame = created_request->IsInPrimaryMainFrame(),
  };

  if (created_request->IsNavigationStarted()) {
    navigation_state_ = NavigationState::kStarted;
    if (created_request->IsSameDocument() &&
        created_request->IsInPrimaryMainFrame()) {
      // For same-doc navigations, we clone the old surface layer and subscribe
      // to the widget host immediately after sending the "CommitNavigation"
      // message. Once the browser receives the renderer's "DidCommitNavigation"
      // message, it is too late to make a clone or subscribe to the widget
      // host.
      MaybeCloneOldSurfaceLayer(
          created_request->GetRenderFrameHost()->GetView());
      SubscribeToNewRenderWidgetHost(created_request.get());
    }
  } else {
    CHECK(!created_request->IsSameDocument());
    CHECK(created_request->IsWaitingForBeforeUnload());
    navigation_state_ = NavigationState::kBeforeUnloadDispatched;
  }
}

BackForwardTransitionAnimator::ComputedAnimationValues
BackForwardTransitionAnimator::ComputeAnimationValues(
    const PhysicsModel::Result& result) {
  ComputedAnimationValues values;

  const auto viewport_width_px = GetViewportWidthPx();
  values.progress =
      std::abs(result.foreground_offset_physical) / viewport_width_px;

  if (nav_direction_ == NavigationDirection::kForward) {
    // The physics model assumes the background comes in from slightly outside
    // the viewport. But in forward navigations the live page is in the
    // background, it starts fully in the viewport, and moves slightly
    // offscreen. So shift the live page so that it starts in the viewport.
    float start_from_origin =
        -PhysicsModel::kScreenshotInitialPositionRatio * viewport_width_px;
    values.live_page_offset_px =
        result.background_offset_physical + start_from_origin;
    // The physics model assumes the foreground starts fully in the viewport and
    // slides out. In a forward navigation the foreground is the screenshot and
    // comes from fully out of the viewport so offset it by the viewport width
    // to make it animate from fully out to fully in.
    values.screenshot_offset_px =
        result.foreground_offset_physical - viewport_width_px;
  } else {
    values.live_page_offset_px = result.foreground_offset_physical;
    values.screenshot_offset_px = result.background_offset_physical;
  }

  // Swipes from the right edge will travel in the opposite direction.
  if (initiating_edge_ == SwipeEdge::RIGHT) {
    values.live_page_offset_px *= -1;
    values.screenshot_offset_px *= -1;
  }

  CHECK_GE(values.progress, 0.f);
  CHECK_LE(values.progress, 1.f);

  return values;
}

cc::UIResourceId BackForwardTransitionAnimator::CreateUIResource(
    cc::UIResourceClient* client) {
  // A Window is detached from the NativeView if the tab is not currently
  // displayed. It would be an error to use any of the APIs in this file.
  ui::WindowAndroid* window = animation_manager_->web_contents_view_android()
                                  ->GetTopLevelNativeWindow();
  CHECK(window);
  // Guaranteed to have a compositor as long as the window is attached.
  ui::WindowAndroidCompositor* compositor = window->GetCompositor();
  CHECK(compositor);
  return static_cast<CompositorImpl*>(compositor)->CreateUIResource(client);
}

void BackForwardTransitionAnimator::DeleteUIResource(
    cc::UIResourceId resource_id) {
  ui::WindowAndroid* window = animation_manager_->web_contents_view_android()
                                  ->GetTopLevelNativeWindow();
  CHECK(window);
  ui::WindowAndroidCompositor* compositor = window->GetCompositor();
  CHECK(compositor);
  static_cast<CompositorImpl*>(compositor)->DeleteUIResource(ui_resource_id_);
}

bool BackForwardTransitionAnimator::SetLayerTransformationAndTickEffect(
    const PhysicsModel::Result& result) {
  // Mirror for RTL if needed and swap the layers for forward navigations.
  ComputedAnimationValues values = ComputeAnimationValues(result);

  screenshot_layer_->SetTransform(
      gfx::Transform::MakeTranslation(values.screenshot_offset_px, 0.f));

  const auto live_page_transform =
      gfx::Transform::MakeTranslation(values.live_page_offset_px, 0.f);
  animation_manager_->web_contents_view_android()
      ->parent_for_web_page_widgets()
      ->SetTransform(live_page_transform);

  if (old_surface_clone_) {
    CHECK(navigation_state_ == NavigationState::kCommitted ||
          navigation_state_ == NavigationState::kStarted)
        << ToString(navigation_state_);
    CHECK_EQ(state_, State::kDisplayingInvokeAnimation);
    old_surface_clone_->SetTransform(live_page_transform);
  } else if (embedder_live_content_clone_) {
    embedder_live_content_clone_->SetTransform(live_page_transform);
  }

  effect_.Tick(GetFittedTimeTicksForForegroundProgress(values.progress));
  return result.done && effect_.keyframe_models().empty();
}

void BackForwardTransitionAnimator::MaybeCloneOldSurfaceLayer(
    RenderWidgetHostViewBase* old_main_frame_view) {
  // The old View must be still alive (and its renderer).
  CHECK(old_main_frame_view);

  CHECK(!old_surface_clone_);

  if (embedder_live_content_clone_) {
    return;
  }

  const auto* old_surface_layer =
      static_cast<RenderWidgetHostViewAndroid*>(old_main_frame_view)
          ->GetSurfaceLayer();
  old_surface_clone_ = cc::slim::SurfaceLayer::Create();
  // Use a zero deadline because this is a copy of a surface being actively
  // shown. The surface textures are ready (i.e. won't be GC'ed) because
  // `old_surface_clone_` references to them.
  old_surface_clone_->SetSurfaceId(old_surface_layer->surface_id(),
                                   cc::DeadlinePolicy::UseSpecifiedDeadline(0));
  old_surface_clone_->SetPosition(old_surface_layer->position());
  old_surface_clone_->SetBounds(old_surface_layer->bounds());
  old_surface_clone_->SetTransform(old_surface_layer->transform());
  old_surface_clone_->SetIsDrawable(true);

  // Inserts the clone layer into the layer tree.
  InsertLayersInOrder();
}

void BackForwardTransitionAnimator::SetUpEmbedderContentLayerIfNeeded(
    SkBitmap bitmap) {
  if (bitmap.empty()) {
    return;
  }
  embedder_live_content_clone_ = cc::slim::UIResourceLayer::Create();
  embedder_live_content_clone_->SetBitmap(bitmap);
  embedder_live_content_clone_->SetIsDrawable(true);
  embedder_live_content_clone_->SetPosition(gfx::PointF(0.f, 0.f));
  embedder_live_content_clone_->SetBounds(
      animation_manager_->web_contents_view_android()
          ->GetNativeView()
          ->GetPhysicalBackingSize());
}

// TODO(crbug.com/350750205): Refactor this function and
// `OnRenderFrameMetadataChangedAfterActivation` to the manager
void BackForwardTransitionAnimator::SubscribeToNewRenderWidgetHost(
    NavigationRequest* navigation_request) {
  CHECK(!new_render_widget_host_);

  if (!navigation_request->GetNavigationEntry()) {
    // Error case: The navigation entry is deleted when the navigation is ready
    // to commit. Abort the transition.
    AbortAnimation();
    return;
  }

  auto* new_host = navigation_request->GetRenderFrameHost();
  CHECK(new_host);
  new_render_widget_host_ = new_host->GetRenderWidgetHost();
  new_render_widget_host_->AddObserver(animation_manager_);

  CHECK_EQ(primary_main_frame_navigation_entry_item_sequence_number_,
           cc::RenderFrameMetadata::kInvalidItemSequenceNumber);

  if (is_copied_from_embedder_) {
    // The embedder will be responsible for cross-fading from the screenshot
    // to the new content. We don't register
    // `RenderFrameMetadataProvider::Observer` and do not set
    // `primary_main_frame_navigation_entry_item_sequence_number_`.
    return;
  }

  new_render_widget_host_->render_frame_metadata_provider()->AddObserver(
      animation_manager_);
  FrameNavigationEntry* frame_nav_entry =
      static_cast<NavigationEntryImpl*>(
          navigation_request->GetNavigationEntry())
          ->GetFrameEntry(new_host->frame_tree_node());
  // This is a session history of the primary main frame. We must have a
  // valid `FrameNavigationEntry`.
  CHECK(frame_nav_entry);
  CHECK_NE(frame_nav_entry->item_sequence_number(), -1);
  primary_main_frame_navigation_entry_item_sequence_number_ =
      frame_nav_entry->item_sequence_number();
}

void BackForwardTransitionAnimator::UnregisterNewFrameActivationObserver() {
  new_render_widget_host_->render_frame_metadata_provider()->RemoveObserver(
      animation_manager_);
  new_render_widget_host_->RemoveObserver(animation_manager_);
  new_render_widget_host_ = nullptr;
}

int BackForwardTransitionAnimator::GetViewportWidthPx() const {
  return animation_manager_->web_contents_view_android()
      ->GetNativeView()
      ->GetPhysicalBackingSize()
      .width();
}

int BackForwardTransitionAnimator::GetViewportHeightPx() const {
  return animation_manager_->web_contents_view_android()
      ->GetNativeView()
      ->GetPhysicalBackingSize()
      .height();
}

void BackForwardTransitionAnimator::StartInputSuppression() {
  CHECK(!ignore_input_scope_);

  ignore_input_scope_.emplace(animation_manager_->web_contents_view_android()
                                  ->web_contents()
                                  ->IgnoreInputEvents(
                                      /*audit_callback=*/std::nullopt));
}

void BackForwardTransitionAnimator::InsertLayersInOrder() {
  // The layer order when navigating backwards (successive lines decrease in
  // z-order):
  //
  //   WebContentsViewAndroid::view_->GetLayer()
  //      |- `embedder_live_content_clone_`
  //      |- `old_surface_clone_` (only set during the invoke animation
  //           and when `embedder_live_content_clone_` is not set).
  //      |- parent_for_web_page_widgets_ (RWHVAndroid, Overscroll etc).
  //      |-   progress_bar_ (child of screenshot_layer_,
  //                          only during invoke animation)
  //      |-   rrect_layer_ (child of screenshot_layer_, if fallback UX is used)
  //      |-   screenshot_scrim_ (child of screenshot_layer_)
  //      |- screenshot_layer_
  //
  // And when navigating forwards:
  //
  //   WebContentsViewAndroid::view_->GetLayer()
  //      |-   progress_bar_
  //      |-   rrect_layer_ (if fallback UX is used)
  //      |-   screenshot_scrim_
  //      |- screenshot_layer_
  //      |- old_surface_clone_
  //      |- parent_for_web_page_widgets_
  //
  // Finally, in both cases -- when the navigation is about to complete -- the
  // screenshot layer is placed over top of the new live page so that the cross
  // fade animation can smoothly transition to the live page:
  //
  //   WebContentsViewAndroid::view_->GetLayer()
  //      |-   screenshot_scrim_
  //      |- screenshot_layer_
  //      |- parent_for_web_page_widgets_

  // This class' layers are removed and reinserted relative to the
  // parent_for_web_page_widgets layer to ensure the ordering is always
  // up-to-date after this call. Remove both layers first, before any
  // re-inserting, to avoid having to bookkeep the changing
  // web_page_widgets_index.
  CHECK(screenshot_layer_);
  if (screenshot_layer_->parent()) {
    screenshot_layer_->RemoveFromParent();
  }

  if (embedder_live_content_clone_) {
    embedder_live_content_clone_->RemoveFromParent();
  } else if (old_surface_clone_) {
    old_surface_clone_->RemoveFromParent();
  }

  cc::slim::Layer* parent_layer =
      animation_manager_->web_contents_view_android()
          ->parent_for_web_page_widgets()
          ->parent();
  const std::vector<scoped_refptr<cc::slim::Layer>> layers =
      parent_layer->children();
  auto itr =
      base::ranges::find(layers, animation_manager_->web_contents_view_android()
                                     ->parent_for_web_page_widgets());
  CHECK(itr != layers.end());
  std::ptrdiff_t web_page_widgets_index = std::distance(layers.begin(), itr);

  // The screenshot layer is shown below the live web page when navigating
  // backwards and above it when navigating forwards. The screenshot is always
  // on top when cross-fading.
  bool screenshot_on_top = nav_direction_ == NavigationDirection::kForward ||
                           state_ == State::kDisplayingCrossFadeAnimation;
  std::ptrdiff_t screenshot_index =
      screenshot_on_top ? web_page_widgets_index + 1 : web_page_widgets_index;
  parent_layer->InsertChild(screenshot_layer_.get(), screenshot_index);

  if (!screenshot_on_top) {
    ++web_page_widgets_index;
  }

  if (embedder_live_content_clone_) {
    // The embedder live content clone is used only when there is a visible
    // native view corresponding to the currently committed navigation entry.
    parent_layer->InsertChild(embedder_live_content_clone_.get(),
                              web_page_widgets_index + 1);
  } else if (old_surface_clone_) {
    // The old page clone is used only when the old live page is swapped out so
    // may be null at other times.

    // The clone is no longer needed when cross-fading - the screenshot layer
    // must always be on top at this time.
    CHECK_NE(state_, State::kDisplayingCrossFadeAnimation);

    // Since the clone represents the old live page it must maintain the
    // ordering relative to the screenshot noted above but must also be shown
    // above the live web page layer. Since the web page widget is already
    // ordered relative to the screenshot, order it directly on top of it.
    parent_layer->InsertChild(old_surface_clone_.get(),
                              web_page_widgets_index + 1);
  }
}

void BackForwardTransitionAnimator::OnPostNavigationFirstFrameTimeout() {
  CHECK_EQ(state_, State::kWaitingForNewRendererToDraw);
  CHECK_EQ(navigation_state_, NavigationState::kCommitted);
  AbortAnimation();
  animation_manager_->OnPostNavigationFirstFrameTimeout();
}

void BackForwardTransitionAnimator::ResetLiveOverlayLayer() {
  if (embedder_live_content_clone_) {
    CHECK(!old_surface_clone_);
    embedder_live_content_clone_->RemoveFromParent();
    embedder_live_content_clone_.reset();
    return;
  }

  // There is no `old_surface_clone_` when navigating from a crashed page.
  if (old_surface_clone_) {
    old_surface_clone_->RemoveFromParent();
    old_surface_clone_.reset();
  }
}

gfx::PointF BackForwardTransitionAnimator::CalculateRRectStartPx() const {
  float y_start = (GetViewportHeightPx() - DipToPx(kRRectSizeDip)) / 2.f;
  /* LTR, left edge back nav. The rrect starts at 25%*W px w.r.t. the
     screenshot.

    screenshot   live page       screenshot                 live page
      ▲                ▲              ▲                        ▲
      │                │              │                        │
    ┌─┼──┌─────────────┼─┐        ┌───┼───────────┌────────────┼──┐
    │    │         │     │        │               │               │
    │    │         │     │        │               │               │
    │    ┌────┐    │     │        │     ┌────┐    │               │
    │    │    │    │     │        │     │    │    │               │
    │25% │    │    │     │        │     │    │    │               │
    │    └────┘    │     │        │     └────┘    │               │
    │    │         │     │        │               │               │
    │    │         │     │        │               │               │
    └────└───────────────┘        └───────────────└───────────────┘
          start                                stop
  */
  if (initiating_edge_ == SwipeEdge::LEFT &&
      nav_direction_ == NavigationDirection::kBackward) {
    return gfx::PointF(std::abs(GetViewportWidthPx() *
                                PhysicsModel::kScreenshotInitialPositionRatio),
                       y_start);
  }
  /* LTR, right edge forward nav. The rrect starts at 0px w.r.t. the screenshot.

  live page              screenshot      live page          screenshot
       ▲                     ▲               ▲                  ▲
       │                     │               │                  │
    ┌──┼───────────┌─────────┼────┐        ┌─┼───┌──────────────┼──┐
    │              │              │        │     │          │      │
    │              │              │        │     │          │      │
    │              │              │        │     │          │      │
    │              ┌─────┐        │        │     │     ┌─────┐     │
    │              │     │        │        │     │     │    ││     │
    │              │     │        │        │     │     │    ││     │
    │              └─────┘        │        │     │     └─────┘     │
    │              │              │        │     │          │      │
    │              │              │        │     │          │      │
    │              │              │        │     │          │      │
    └──────────────└──────────────┘        └─────└──────────┴──────┘
              start                                stop
  */
  else if (initiating_edge_ == SwipeEdge::RIGHT &&
           nav_direction_ == NavigationDirection::kForward) {
    return gfx::PointF(0.f, y_start);
  }
  /* RTL, right edge back nav. The rrect starts at (1-25%)*W px w.r.t the
     screenshot layer.

    live page          screenshot       live page             screenshot
        ▲                  ▲                ▲                      ▲
        │                  │                │                      │
      ┌─┼───┌──────────────┼──┐         ┌───┼────────────┌─────────┼──────┐
      │ │   │          │   │  │         │   │            │         │      │
      │     │          │      │         │                │                │
      │     │          │  25% │         │                │                │
      │     │          ┌──────┐         │                │    ┌──────┐    │
      │     │          │      │         │                │    │      │    │
      │     │          │      │         │                │    │      │    │
      │     │          └──────┘         │                │    └──────┘    │
      │     │          │      │         │                │                │
      │     │          │      │         │                │                │
      │     │          │      │         │                │                │
      └─────└──────────┴──────┘         └────────────────└────────────────┘
             start                                   stop
  */
  else if (initiating_edge_ == SwipeEdge::RIGHT &&
           nav_direction_ == NavigationDirection::kBackward) {
    return gfx::PointF(
        GetViewportWidthPx() -
            std::abs(GetViewportWidthPx() *
                     PhysicsModel::kScreenshotInitialPositionRatio),
        y_start);
  }
  /* RTL, left edge forward nav. The rrect starts at W-w px w.r.t the
     screenshot, where w is the width of the rrect.

       screenshot          live page    screenshot           live page
        ▲                     ▲               ▲                  ▲
        │                     │               │                  │
     ┌──┼───────────┌─────────┼────┐        ┌─┼───┌──────────────┼──┐
     │  │           │         │    │        │ │   │          │   │  │
     │              │              │        │     │          │      │
     │              │              │        │     │          │      │
     │        ┌─────┐              │        │     ┌─────┐    │      │
     │        │     │              │        │     │     │    │      │
     │        │     │              │        │     │     │    │      │
     │        └─────┘              │        │     └─────┘    │      │
     │              │              │        │     │          │      │
     │              │              │        │     │          │      │
     └──────────────└──────────────┘        └─────└──────────┴──────┘
                start                                stop
  */
  else if (initiating_edge_ == SwipeEdge::LEFT &&
           nav_direction_ == NavigationDirection::kForward) {
    return gfx::PointF(GetViewportWidthPx() - DipToPx(kRRectSizeDip), y_start);
  } else {
    NOTREACHED_NORETURN();
  }
}

gfx::PointF BackForwardTransitionAnimator::CalculateRRectEndPx() const {
  return gfx::PointF((GetViewportWidthPx() - DipToPx(kRRectSizeDip)) / 2.f,
                     (GetViewportHeightPx() - DipToPx(kRRectSizeDip)) / 2.f);
}

int BackForwardTransitionAnimator::DipToPx(int dip) const {
  return gfx::ScaleToFlooredSize(gfx::Size(dip, dip), device_scale_factor_)
      .width();
}

}  // namespace content