chromium/content/browser/navigation_transitions/back_forward_transition_animator.h

// 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.

#ifndef CONTENT_BROWSER_NAVIGATION_TRANSITIONS_BACK_FORWARD_TRANSITION_ANIMATOR_H_
#define CONTENT_BROWSER_NAVIGATION_TRANSITIONS_BACK_FORWARD_TRANSITION_ANIMATOR_H_

#include "cc/resources/ui_resource_client.h"
#include "content/browser/navigation_transitions/physics_model.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/back_forward_transition_animation_manager.h"
#include "content/public/browser/render_frame_metadata_provider.h"
#include "content/public/browser/render_widget_host_observer.h"
#include "content/public/browser/web_contents_observer.h"
#include "ui/android/view_android_observer.h"
#include "ui/android/window_android_observer.h"
#include "ui/events/back_gesture_event.h"
#include "ui/gfx/animation/keyframe/keyframe_effect.h"

namespace cc::slim {
class Layer;
class SolidColorLayer;
class SurfaceLayer;
class UIResourceLayer;
}

namespace content {

class NavigationControllerImpl;
class NavigationEntryScreenshot;
class WebContentsViewAndroid;
class BackForwardTransitionAnimationManagerAndroid;
class ProgressBar;
class RenderWidgetHostImpl;
class RenderFrameHostImpl;

// This class listens to gesture events for navigating the session history and
// updates the UI in response. It is 1:1 with a single gesture, i.e. each time
// the user touches the screen to start a gesture a new instance is created.
class CONTENT_EXPORT BackForwardTransitionAnimator
    : public gfx::FloatAnimationCurve::Target,
      public gfx::TransformAnimationCurve::Target {
 public:
  // Identifies the different stages of the animation that this manager is in.
  enum class State {
    // Set immediately when `OnGestureStarted` is called. Indicates that the
    // user has started swiping from the edge of the screen. The manager remains
    // in this state until the user has lifted the finger from the screen, to
    // either start the history navigation or not start it.
    kStarted = 0,

    // No explicit state while the user swipes across the screen, since there
    // is no stateful changes during the in-progress transition period.

    // Set when `OnGestureCancelled` is called, signaling the user has decided
    // to not start the history navigation.
    //
    // Also set when the gesture-initiated navigation is aborted or cancelled.
    // In this state, an animation is being displayed to dismiss the screenshot
    // and bring the old page back to the viewport.
    //
    // Also set when the active page has a BeforeUnload handler and we need to
    // animate the active page back so the user can interact with the
    // BeforeUnload prompt. TODO(liuwilliam): Worth considering a
    // `kDisplayingCancelAnimationForBeforeUnload` to reduce the compexity in
    // the `State`'s transition.
    kDisplayingCancelAnimation,

    // Set after the browser has dispatched BeforeUnload IPC to the renderer and
    // is waiting for the response, and the cancel animation has brought back
    // the active page to the center of the viewport. This is an optional state:
    // if the cancel animation hasn't finished before the renderer has
    // responded, we will skip this state.
    kWaitingForBeforeUnloadResponse,

    // TODO(crbug.com/40896070): If we were to bring the active page back
    // to let the user interact with the prompt (e.g., camera access), we need a
    // state for that.

    // Set when `OnGestureInvoked` is called, signaling the user has decided
    // to start the history navigation. Animations are displayed to bring the
    // screenshot to the center of the viewport, and to bring the old page
    // completely out of the viewport.
    //
    // The gesture-initiated history navigation starts at the beginning of this
    // state. The same navigation is finished in the browser at the end of this
    // state.
    //
    // Internally, this state covers `PhysicsModel`'s commit-pending spring
    // and invoke spring. We don't differentiate commit-pending vs invoke as
    // commit-pending is designed to be a `PhysicsModel` internal state.
    kDisplayingInvokeAnimation,

    // An optional state only reachable from `kDisplayingInvokeAnimation`: at
    // the end of the invoke animation, the screenshot is centered at the
    // viewport. Before the new page is ready to be presented the user, the
    // screenshot will persist at the viewport center. The screenshot is only
    // crossfaded out after the new renderer is ready to be presented to the
    // user, which is signalled via
    // `OnRenderFrameMetadataChangedAfterActivation()`, meaning viz has
    // processed a new compositor frame submitted by the new renderer.
    //
    // If `OnRenderFrameMetadataChangedAfterActivation()` is received before
    // the end of `kDisplayingInvokeAnimation`, this state will be skipped
    // completely.
    kWaitingForNewRendererToDraw,

    // A state only reachable from `kDisplayingInvokeAnimation`: at
    // the end of the invoke animation, the animator is waiting for the
    // embedder content to be fully visible. The animator will continue or
    // end after the content becomes fully visible.
    kWaitingForContentForNavigationEntryShown,

    // Reachable from the end of `kDisplayingInvokeAnimation` or from
    // `kWaitingForNewRendererToDraw`. Cross-fading from the screenshot to the
    // new page.
    kDisplayingCrossFadeAnimation,

    // One of the two terminal states of the animation manager. We reach this
    // state when all the animations are finished in the UI. The manager remains
    // in this state until it is destroyed.
    kAnimationFinished,

    // Another terminal state indicating that we have to abort the animated
    // transition. This can happen, for example, when a secondary navigation
    // commits mid-animation, or when Chrome is backgrounded during a
    // transition.
    kAnimationAborted,
  };
  static std::string ToString(State state);

  // To create the `BackForwardTransitionAnimator`. Tests can override this
  // factory to supply a customized version of `BackForwardTransitionAnimator`.
  class Factory {
   public:
    Factory() = default;
    Factory(const Factory&) = delete;
    Factory& operator=(const Factory&) = delete;
    virtual ~Factory() = default;

    virtual std::unique_ptr<BackForwardTransitionAnimator> Create(
        WebContentsViewAndroid* web_contents_view_android,
        NavigationControllerImpl* controller,
        const ui::BackGestureEvent& gesture,
        BackForwardTransitionAnimationManager::NavigationDirection
            nav_direction,
        ui::BackGestureEventSwipeEdge initiating_edge,
        NavigationEntryImpl* destination_entry,
        SkBitmap embedder_content,
        BackForwardTransitionAnimationManagerAndroid* animation_manager);
  };

  BackForwardTransitionAnimator(const BackForwardTransitionAnimator&) = delete;
  BackForwardTransitionAnimator& operator=(
      const BackForwardTransitionAnimator&) = delete;
  ~BackForwardTransitionAnimator() override;

  // Mirrors the APIs on `BackForwardTransitionAnimationManager`.
  // Some of them are virtual for testing purposes.
  void OnGestureProgressed(const ui::BackGestureEvent& gesture);
  void OnGestureCancelled();
  void OnGestureInvoked();
  void OnContentForNavigationEntryShown();
  BackForwardTransitionAnimationManager::AnimationStage
  GetCurrentAnimationStage();
  virtual void OnAnimate(base::TimeTicks frame_begin_time);
  void OnRenderWidgetHostDestroyed(RenderWidgetHost* widget_host);
  virtual void OnRenderFrameMetadataChangedAfterActivation(
      base::TimeTicks activation_time);
  virtual void DidStartNavigation(NavigationHandle* navigation_handle);
  virtual void ReadyToCommitNavigation(NavigationHandle* navigation_handle);
  virtual void DidFinishNavigation(NavigationHandle* navigation_handle);
  void OnDidNavigatePrimaryMainFramePreCommit(
      NavigationRequest* navigation_request,
      RenderFrameHostImpl* old_host,
      RenderFrameHostImpl* new_host);
  void OnNavigationCancelledBeforeStart(NavigationHandle* navigation_handle);

  // Notifies when the transition needs to be aborted.
  void AbortAnimation();

  [[nodiscard]] bool IsTerminalState();

  cc::slim::Layer* screenshot_layer_for_testing() const {
    return screenshot_layer_.get();
  }
  cc::slim::SolidColorLayer* scrim_layer_for_testing() const {
    return screenshot_scrim_.get();
  }
  cc::slim::SurfaceLayer* clone_layer_for_testing() const {
    return old_surface_clone_.get();
  }
  cc::slim::SolidColorLayer* rrect_layer_for_testing() const {
    return rounded_rectangle_.get();
  }
  ProgressBar* progress_bar_for_testing() const { return progress_bar_.get(); }
  cc::slim::UIResourceLayer* embedder_live_content_clone_for_testing() const {
    return embedder_live_content_clone_.get();
  }

  base::OneShotTimer* dismiss_screenshot_timer_for_testing() {
    return &dismiss_screenshot_timer_;
  }

 protected:
  BackForwardTransitionAnimator(
      WebContentsViewAndroid* web_contents_view_android,
      NavigationControllerImpl* controller,
      const ui::BackGestureEvent& gesture,
      BackForwardTransitionAnimationManager::NavigationDirection nav_direction,
      ui::BackGestureEventSwipeEdge initiating_edge,
      NavigationEntryImpl* destination_entry,
      SkBitmap embedder_content,
      BackForwardTransitionAnimationManagerAndroid* animation_manager);

  // `gfx::FloatAnimationCurve::Target`:
  void OnFloatAnimated(const float& value,
                       int target_property_id,
                       gfx::KeyframeModel* keyframe_model) override;

  // `gfx::TransformAnimationCurve::Target`:
  void OnTransformAnimated(const gfx::TransformOperations& transform,
                           int target_property_id,
                           gfx::KeyframeModel* keyframe_model) override;

  // Called when each animation finishes. Advances `this` into the next state.
  // Being virtual for testing.
  virtual void OnCancelAnimationDisplayed();
  virtual void OnInvokeAnimationDisplayed();
  virtual void OnCrossFadeAnimationDisplayed();

  static bool CanAdvanceTo(State from, State to);

  enum class NavigationState {
    // Navigation has not begun.
    kNotStarted = 0,

    // Two states to track the BeforeUnload handler. They are optional if the
    // active page does not have a BeforeUnload handler.
    kBeforeUnloadDispatched,
    // This state functions as a boolean flag to distinguish how we get to
    // `kStarted`:
    // - From `kNotStarted` as regular navigations, or;
    // - From `kBeforeUnloadAckedProceed` as navigations with BeforeUnload
    //   handlers.
    // It's only set when the browser receives the renderer's ack with proceed,
    // and advances to `kStarted` when the navigation starts, which happens
    // within an atomic stack.
    kBeforeUnloadAckedProceed,

    // The navigation is cancelled before it starts. Terminal state 1/3.
    // Reachable from `kNotStarted` and `kBeforeUnloadDispatched`.
    kCancelledBeforeStart,
    // The navigation has started in the browser.
    kStarted,
    // The navigation has either committed to a new doc, or an error page.
    // Terminal state 2/3.
    kCommitted,
    // The navigation has been cancelled (cancelled by a secondary navigation,
    // or aborted by the user). Terminal state 3/3.
    kCancelled,
  };
  static std::string ToString(NavigationState state);

  ui::BackGestureEventSwipeEdge initiating_edge() const {
    return initiating_edge_;
  }

  State state_;

  // The destination `FrameNavigationEntry::item_sequence_number()` of the
  // gesture back navigation in the primary main frame. Set when the browser
  // tells the renderer to commit the navigation.
  int64_t primary_main_frame_navigation_entry_item_sequence_number_ =
      cc::RenderFrameMetadata::kInvalidItemSequenceNumber;

 private:
  // Initializes `effect_` for the scrim and cross-fade animation.
  void InitializeEffectForGestureProgressAnimation();
  void InitializeEffectForCrossfadeAnimation();

  // Advance current `state_` to `state`.
  void AdvanceAndProcessState(State state);

  // Let this manager respond to the current `state_`.
  void ProcessState();

  // Initializes the `ui_resource_layer_` and sets up the layer tree.
  void SetupForScreenshotPreview(SkBitmap embedder_content);

  // Sets the progress bar shown during the invoke phase of the animation.
  void SetupProgressBar();

  // Start the session history navigation, and start tracking the created
  // `NavigationRequests` by their IDs. Returns true if the requests are
  // successfully created and false otherwise. The caller should play the invoke
  // or cancel animation based on the return value.
  [[nodiscard]] bool StartNavigationAndTrackRequest();
  void TrackRequest(base::WeakPtr<NavigationRequest> created_request);

  struct ComputedAnimationValues {
    // The offset that will be applied to the live, outgoing page.
    float live_page_offset_px = 0.f;
    // The offset that will be applied to the incoming screenshot layer.
    float screenshot_offset_px = 0.f;
    // The current progress of the animation, running from 0 to 1.
    float progress = 0.f;
  };

  // The physics model is agnostic of UI writing mode (LTR vs RTL) as well as
  // navigation direction and functions in terms of a spring on the left side
  // applied to a layer moving to the right. This method transforms the physics
  // result values into values usable by the animator.
  ComputedAnimationValues ComputeAnimationValues(
      const PhysicsModel::Result& result);

  // Forwards the calls to `CompositorImpl`.
  cc::UIResourceId CreateUIResource(cc::UIResourceClient* client);
  void DeleteUIResource(cc::UIResourceId resource_id);

  // Apply the `result` to the screenshot and the web page, and tick the
  // animation effector. Returns a boolean indicating if both the `PhysicsModel`
  // and the `gfx::KeyFrameModels` have finished playing.
  [[nodiscard]] bool SetLayerTransformationAndTickEffect(
      const PhysicsModel::Result& result);

  void MaybeCloneOldSurfaceLayer(RenderWidgetHostViewBase* old_main_frame_view);

  void SetUpEmbedderContentLayerIfNeeded(SkBitmap embedder_content);

  // Called when the navigation is ready to be committed in the renderer.
  void SubscribeToNewRenderWidgetHost(NavigationRequest* navigation_request);

  void UnregisterNewFrameActivationObserver();

  int GetViewportWidthPx() const;
  int GetViewportHeightPx() const;

  void StartInputSuppression();

  void InsertLayersInOrder();

  // Dispatched when `dismiss_screenshot_timer_` fires, to remove the stale
  // screenshot after the screenshot is fully centered because the new Document
  // hasn't produced a frame yet.
  void OnPostNavigationFirstFrameTimeout();

  void ResetLiveOverlayLayer();

  // Calculate the start and end position of the rrect for the fallback UX, in
  // physical pixels.
  gfx::PointF CalculateRRectStartPx() const;
  gfx::PointF CalculateRRectEndPx() const;

  int DipToPx(int dip) const;

  const BackForwardTransitionAnimationManager::NavigationDirection
      nav_direction_;

  const ui::BackGestureEventSwipeEdge initiating_edge_;

  // The ID of the destination `NavigationEntry`. Constant through out the
  // lifetime of a gesture so we are guaranteed to target the correct entry.
  // This is also guaranteed to be equal to `screenshot_->navigation_entry_id()`
  // once `screenshot_` is set.
  const int destination_entry_id_;

  // The manager back-pointer. Guaranteed to outlive the impl.
  const raw_ptr<BackForwardTransitionAnimationManagerAndroid>
      animation_manager_;

  // Track the ID of the `NavigationRequest` created by the gesture back
  // navigation in the primary main frame or in the subframe:
  // - If a request is created in the primary main frame, we won't track any of
  // the subframe requests (i.e., a fragment navigation in the primary main
  // frame and cross-doc navigations in the subframes).
  // - Else, we track the subframe request.
  // - For any navigation with more than one subframe requests, the transition
  // is aborted.
  struct TrackedRequest {
    int64_t navigation_id;
    bool is_primary_main_frame;
  };
  std::optional<TrackedRequest> tracked_request_;

  // Set when a navigation is being started.
  bool is_starting_navigation_ = false;

  // The unique id assigned to `screenshot_`.
  cc::UIResourceId ui_resource_id_ =
      cc::UIResourceClient::kUninitializedUIResourceId;

  // New layer for the scrim. Always on top of the `ui_resource_layer_`.
  scoped_refptr<cc::slim::SolidColorLayer> screenshot_scrim_;

  // New layer for `screenshot_`.
  scoped_refptr<cc::slim::Layer> screenshot_layer_;

  // A copy of old surface, covering the entire old page from when the
  // navigation commits to the end of the invoke animation (where the old page
  // is completely out of the viewport).
  // - For cross-RFH navigations, it is cloned before RFH swap;
  // - For same-RFH and same-doc navigations, it is cloned immediately after we
  //   tell the renderer to commit the navigation.
  scoped_refptr<cc::slim::SurfaceLayer> old_surface_clone_;

  // A copy of the embedder content to show the content from the embedder side.
  // Only one of old_surface_clone_ or embedder_live_content_clone_ will be set.
  scoped_refptr<cc::slim::UIResourceLayer> embedder_live_content_clone_;

  // The pre-captured screenshot used for previewing. The ownership of the
  // screenshot is transferred from the cache to this manager when the gesture
  // starts. If the user decides not to start the history navigation, or the
  // gesture navigation starts but cancelled by another navigation, the
  // screenshot will be placed back into the cache.
  //
  // Let this animation manager take ownership of the screenshot during the
  // animation. This is to keep the cache from evicting the screenshot while
  // it's being displayed in the UI.
  std::unique_ptr<NavigationEntryScreenshot> screenshot_;

  // If `screenshot_` is supplied by the embedder.
  const bool is_copied_from_embedder_;

  // The scale factor is constant per gesture.
  const float device_scale_factor_;

  // Color and positional information to compose a fallback screenshot.
  struct FallbackUX {
    BackForwardTransitionAnimationManager::FallbackUXConfig color_config;
    // The start and stop positions of the rounded rectangle, with respect to
    // its parent (the screenshot layer).
    gfx::PointF start_px;
    gfx::PointF end_px;
  };
  std::optional<FallbackUX> fallback_ux_;
  // The rounded rectangle specified by `fallback_ux_`. It embeds the favicon,
  // and is child of the screenshot layer. Need the reference here because the
  // animation timeline of the rounded rectangle and the favicon is different
  // from the screenshot.
  scoped_refptr<cc::slim::SolidColorLayer> rounded_rectangle_;

  // Tracks various state of the navigation request associated with this
  // gesture. Only set if the navigation request is successfully created.
  NavigationState navigation_state_ = NavigationState::kNotStarted;

  // If viz has already activated a frame for the new page before the invoke
  // animation finishes, we set this bit so we can start the crossfade animation
  // immediately after the invoke animation.
  bool viz_has_activated_first_frame_ = false;

  // The widget host for the new page.
  // - For cross-doc navigations, it is set when the browser receives the
  //   "DidCommit" message.
  // - For same-doc navigations, it is set immediately after we tell the
  //   renderer to commit the navigation.
  //
  // It listens to the first
  // `OnRenderFrameMetadataChangedAfterActivation()` on the new widget host.
  // This first notification signals that viz has processed a frame submitted by
  // the renderer, at which we can safely cross-fade from the screenshot to the
  // new page.
  //
  // Stays null for 204/205/Download, or for cancelled navigations. Also reset
  // to null when the tracked `RenderWidgetHost` is destroyed.
  raw_ptr<RenderWidgetHostImpl> new_render_widget_host_;

  // Responsible for the non-transformational animations (e.g., scrim and
  // cross-fade), and the position of the rounded rectangle (when fallback UX is
  // used).
  gfx::KeyframeEffect effect_;

  // Responsible for the transformational animations of the live page and the
  // screenshot.
  PhysicsModel physics_model_;

  // Set by the latest `OnGestureProgressed()`.
  ui::BackGestureEvent latest_progress_gesture_;

  // The indeterminate progress bar shown during the invoke animation.
  std::unique_ptr<ProgressBar> progress_bar_;

  // A transition suppresses sending input events to the renderer during the
  // animation.
  std::optional<WebContentsImpl::ScopedIgnoreInputEvents> ignore_input_scope_;

  // A timer to dismiss the potentially stale screenshot, after screenshot is
  // fully centered (at the end of the invoke animation).
  base::OneShotTimer dismiss_screenshot_timer_;

  base::WeakPtrFactory<BackForwardTransitionAnimator> weak_ptr_factory_{this};
};

}  // namespace content

#endif  // CONTENT_BROWSER_NAVIGATION_TRANSITIONS_BACK_FORWARD_TRANSITION_ANIMATOR_H_