// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/page_lifecycle_state_manager.h"

#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "base/state_transitions.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/render_process_host.h"
#include "services/service_manager/public/cpp/interface_provider.h"

namespace {
// ASAN builds are slow and we see flakes caused by reaching this timeout. 6s
// was not enough to stop the flakes. Trying 12s just to ensure that there isn't
// something else going on that we don't understand.
// See https://crbug.com/1224355.
#if defined(ADDRESS_SANITIZER)
constexpr base::TimeDelta kBackForwardCacheTimeout = base::Seconds(12);
#else
constexpr base::TimeDelta kBackForwardCacheTimeout = base::Seconds(3);
#endif
base::TimeDelta GetBackForwardCacheEntryTimeout() {
  if (base::FeatureList::IsEnabled(features::kBackForwardCacheEntryTimeout)) {
    return kBackForwardCacheTimeout;
  } else {
    return base::TimeDelta::Max();
  }
}
}

namespace content {

PageLifecycleStateManager::TestDelegate::TestDelegate() = default;

PageLifecycleStateManager::TestDelegate::~TestDelegate() = default;

void PageLifecycleStateManager::TestDelegate::OnLastAcknowledgedStateChanged(
    const blink::mojom::PageLifecycleState& old_state,
    const blink::mojom::PageLifecycleState& new_state) {}

void PageLifecycleStateManager::TestDelegate::OnUpdateSentToRenderer(
    const blink::mojom::PageLifecycleState& new_state) {}

void PageLifecycleStateManager::TestDelegate::OnDeleted() {}

PageLifecycleStateManager::PageLifecycleStateManager(
    RenderViewHostImpl* render_view_host_impl,
    blink::mojom::PageVisibilityState frame_tree_visibility)
    : frame_tree_visibility_(frame_tree_visibility),
      render_view_host_impl_(render_view_host_impl) {
  last_acknowledged_state_ = CalculatePageLifecycleState();
  last_state_sent_to_renderer_ = last_acknowledged_state_.Clone();
}

PageLifecycleStateManager::~PageLifecycleStateManager() {
  if (test_delegate_)
    test_delegate_->OnDeleted();
}

void PageLifecycleStateManager::SetIsFrozen(bool frozen) {
  if (frozen_explicitly_ == frozen) {
    return;
  }
  frozen_explicitly_ = frozen;

  SendUpdatesToRendererIfNeeded(
      /*page_restore_params=*/nullptr, base::NullCallback());
}

void PageLifecycleStateManager::SetFrameTreeVisibility(
    blink::mojom::PageVisibilityState visibility) {
  if (frame_tree_visibility_ == visibility)
    return;

  frame_tree_visibility_ = visibility;

  if (visibility == blink::mojom::PageVisibilityState::kVisible) {
    // Unset `frozen_explicitly_` when the page is shown, to reflect that the
    // Blink page scheduler unfreezes the page in that situation. This ensures
    // that the page is frozen if SetIsFrozen(true) is called while the page is
    // hidden in the future (SetIsFrozen(true) no-ops if `frozen_explicitly_` is
    // true).
    frozen_explicitly_ = false;
  }

  SendUpdatesToRendererIfNeeded(
      /*page_restore_params=*/nullptr, base::NullCallback());
  // TODO(yuzus): When a page is frozen and made visible, the page should
  // automatically resume.
}

void PageLifecycleStateManager::SetBackForwardCacheEntered(
    BackForwardCacheEntered entered) {
  static const base::NoDestructor<
      base::StateTransitions<BackForwardCacheEntered>>
      transitions(base::StateTransitions<BackForwardCacheEntered>({
          {BackForwardCacheEntered::kNo, {BackForwardCacheEntered::kEntering}},
          {BackForwardCacheEntered::kEntering,
           {BackForwardCacheEntered::kNo, BackForwardCacheEntered::kEntered}},
          {BackForwardCacheEntered::kEntered, {BackForwardCacheEntered::kNo}},
      }));
  CHECK_STATE_TRANSITION(transitions, back_forward_cache_entered_, entered);
  back_forward_cache_entered_ = entered;
}

void PageLifecycleStateManager::SetIsInBackForwardCache(
    bool is_in_back_forward_cache,
    blink::mojom::PageRestoreParamsPtr page_restore_params) {
  if (IsInBackForwardCache() == is_in_back_forward_cache) {
    return;
  }
  // Prevent races by waiting for confirmation that the renderer will no longer
  // evict the page before allowing it to exit the back-forward cache
  CHECK(is_in_back_forward_cache || !last_acknowledged_state_->eviction_enabled,
        base::NotFatalUntil::M158);
  eviction_enabled_ = is_in_back_forward_cache;
  if (is_in_back_forward_cache) {
    SetBackForwardCacheEntered(BackForwardCacheEntered::kEntering);
    // When a page is put into BackForwardCache, the page can run a busy loop.
    // Set a timeout monitor to check that the transition finishes within the
    // time limit.
    back_forward_cache_timeout_monitor_.Start(
        FROM_HERE, GetBackForwardCacheEntryTimeout(),
        base::BindOnce(&PageLifecycleStateManager::OnBackForwardCacheTimeout,
                       weak_ptr_factory_.GetWeakPtr()));
    pagehide_dispatch_ = blink::mojom::PagehideDispatch::kDispatchedPersisted;
  } else {
    CHECK(page_restore_params, base::NotFatalUntil::M158);
    // When a page is restored from the back-forward cache, we should reset this
    // state so that it behaves correctly next time navigation occurs.
    pagehide_dispatch_ = blink::mojom::PagehideDispatch::kNotDispatched;
    SetBackForwardCacheEntered(BackForwardCacheEntered::kNo);
  }

  SendUpdatesToRendererIfNeeded(std::move(page_restore_params),
                                base::NullCallback());
}

blink::mojom::PageLifecycleStatePtr
PageLifecycleStateManager::SetPagehideDispatchDuringNewPageCommit(
    bool persisted) {
  pagehide_dispatch_ =
      persisted ? blink::mojom::PagehideDispatch::kDispatchedPersisted
                : blink::mojom::PagehideDispatch::kDispatchedNotPersisted;
  // We've only modified |pagehide_dispatch_| here, but the "visibility"
  // property of |last_state_sent_to_renderer_| calculated from
  // CalculatePageLifecycleState() below will be set to kHidden because it
  // depends on the value of |pagehide_dispatch_|.
  last_state_sent_to_renderer_ = CalculatePageLifecycleState();
  CHECK_EQ(last_state_sent_to_renderer_->visibility,
           blink::mojom::PageVisibilityState::kHidden,
           base::NotFatalUntil::M158);

  // We don't need to call SendUpdatesToRendererIfNeeded() because the update
  // will be sent through an OldPageInfo parameter in the CommitNavigation IPC.
  return last_state_sent_to_renderer_.Clone();
}

void PageLifecycleStateManager::DidSetPagehideDispatchDuringNewPageCommit(
    blink::mojom::PageLifecycleStatePtr acknowledged_state) {
  CHECK_EQ(acknowledged_state->visibility,
           blink::mojom::PageVisibilityState::kHidden,
           base::NotFatalUntil::M158);
  CHECK_NE(acknowledged_state->pagehide_dispatch,
           blink::mojom::PagehideDispatch::kNotDispatched,
           base::NotFatalUntil::M158);
  OnPageLifecycleStateChanged(std::move(acknowledged_state));
}

void PageLifecycleStateManager::SetIsLeavingBackForwardCache(
    base::OnceClosure done_cb) {
  CHECK(IsInBackForwardCache(), base::NotFatalUntil::M158);
  eviction_enabled_ = false;
  SendUpdatesToRendererIfNeeded(nullptr, std::move(done_cb));
}

bool PageLifecycleStateManager::RendererExpectedToSendChannelAssociatedIpcs()
    const {
  // eviction_enabled_ => IsInBackForwardCache()
  CHECK(!eviction_enabled_ || IsInBackForwardCache(),
        base::NotFatalUntil::M158);
  return !eviction_enabled_ || !last_acknowledged_state_->eviction_enabled;
}

void PageLifecycleStateManager::SendUpdatesToRendererIfNeeded(
    blink::mojom::PageRestoreParamsPtr page_restore_params,
    base::OnceClosure done_cb) {
  if (!render_view_host_impl_->GetAssociatedPageBroadcast()) {
    // TODO(crbug.com/40158974): For some tests, |render_view_host_impl_|
    // does not have the associated page.
    if (done_cb) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(done_cb));
    }
    return;
  }

  auto new_state = CalculatePageLifecycleState();
  if (last_state_sent_to_renderer_ &&
      last_state_sent_to_renderer_.Equals(new_state)) {
    // TODO(yuzus): Send updates to renderer only when the effective state (per
    // page lifecycle state) has changed since last sent to renderer. It is
    // possible that the web contents state has changed but the effective state
    // has not.
  }

  last_state_sent_to_renderer_ = new_state.Clone();
  auto state = new_state.Clone();

  if (test_delegate_)
    test_delegate_->OnUpdateSentToRenderer(*last_state_sent_to_renderer_);

  render_view_host_impl_->GetAssociatedPageBroadcast()->SetPageLifecycleState(
      std::move(state), std::move(page_restore_params),
      base::BindOnce(
          &PageLifecycleStateManager::OnSetPageLifecycleStateResponse,
          weak_ptr_factory_.GetWeakPtr(), std::move(new_state),
          std::move(done_cb)));
}

blink::mojom::PageLifecycleStatePtr
PageLifecycleStateManager::CalculatePageLifecycleState() {
  auto state = blink::mojom::PageLifecycleState::New();
  state->is_in_back_forward_cache = IsInBackForwardCache();
  state->is_frozen = IsInBackForwardCache() || frozen_explicitly_;
  state->pagehide_dispatch = pagehide_dispatch_;
  // If a page is stored in the back-forward cache, or we have already
  // dispatched/are dispatching pagehide for the page, it should be treated as
  // "hidden" regardless of what |frame_tree_visibility_| is set to.
  state->visibility =
      (IsInBackForwardCache() ||
       pagehide_dispatch_ != blink::mojom::PagehideDispatch::kNotDispatched)
          ? blink::mojom::PageVisibilityState::kHidden
          : frame_tree_visibility_;
  state->eviction_enabled = eviction_enabled_;
  return state;
}

void PageLifecycleStateManager::OnPageLifecycleStateChanged(
    blink::mojom::PageLifecycleStatePtr acknowledged_state) {
  blink::mojom::PageLifecycleStatePtr old_state =
      std::move(last_acknowledged_state_);

  last_acknowledged_state_ = std::move(acknowledged_state);

  // The renderer's acked state is moving from `is_in_back_forward_cache` false
  // to true. This should only happen as a result of a response to
  // `SendPageLifecycleUpdate`. We ignore updates that are not changing the
  // `is_in_back_forward_cache` state.
  if (!old_state->is_in_back_forward_cache &&
      last_acknowledged_state_->is_in_back_forward_cache) {
    SetBackForwardCacheEntered(BackForwardCacheEntered::kEntered);

    // TODO(crbug.com/41494183): currently after the navigation, the old
    // RenderViewHost is marked as inactive.
    // `RenderViewHostImpl::GetMainRenderFrameHost()` will return nullptr. This
    // prevents us from getting the RenderFrameHost even if the main frame of
    // this RenderViewHost is stored in BFCache. Now we are getting the
    // RenderFrameHost from the BackForwardCacheImpl as a workaround, but
    // eventually we might allow getting the RenderFrameHost from a
    // RenderViewHost that's in BFCache.
    if (render_view_host_impl_->frame_tree()->is_primary()) {
      for (auto* entry :
           render_view_host_impl_->frame_tree()
               ->controller()
               .GetBackForwardCache()
               .GetEntriesForRenderViewHostImpl(render_view_host_impl_)) {
        if (entry->render_frame_host()->LoadedWithCacheControlNoStoreHeader()) {
          // If the BFCached document was loaded with "Cache-control: no-store"
          // header, we clear the fallback surface and force the browser to
          // embed a completely new surface when this page is activated from
          // BFCache. This avoids displaying sensitive information between it's
          // restored and the `pageshow` handler completes.
          RenderWidgetHostViewBase* rwhv = render_view_host_impl_->GetWidget()
                                               ->GetRenderWidgetHostViewBase();
          if (rwhv) {
            rwhv->InvalidateLocalSurfaceIdAndAllocationGroup();
            rwhv->ClearFallbackSurfaceForCommitPending();
          }
        }
      }
    }
  }

  // Call |MaybeEvictFromBackForwardCache| after setting
  // |last_acknowledged_state_|.
  // Features which can be cleaned by the page are taken into account only
  // after the 'pagehide' handlers have run. As we might have just received
  // an acknowledgement from the renderer that these handlers have run, call
  // |MaybeEvictFromBackForwardCache| in case we need to start taking these
  // features into account.
  render_view_host_impl_->MaybeEvictFromBackForwardCache();

  // A page that has not yet received an acknowledgement from renderer is not
  // counted against the cache size limit because it might still be ineligible
  // for caching after the ack, i.e., after running handlers. After it receives
  // the ack and we call |MaybeEvictFromBackForwardCache()|, we know whether it
  // is eligible for caching and we should reconsider the cache size limits
  // again.
  render_view_host_impl_->EnforceBackForwardCacheSizeLimit();

  if (last_acknowledged_state_->is_in_back_forward_cache) {
    back_forward_cache_timeout_monitor_.Stop();
  }

  if (test_delegate_) {
    test_delegate_->OnLastAcknowledgedStateChanged(*old_state,
                                                   *last_acknowledged_state_);
  }
}

void PageLifecycleStateManager::OnSetPageLifecycleStateResponse(
    blink::mojom::PageLifecycleStatePtr acknowledged_state,
    base::OnceClosure done_cb) {
  OnPageLifecycleStateChanged(std::move(acknowledged_state));
  if (done_cb)
    std::move(done_cb).Run();
}

void PageLifecycleStateManager::OnBackForwardCacheTimeout() {
  CHECK(!last_acknowledged_state_->is_in_back_forward_cache,
        base::NotFatalUntil::M158);
  render_view_host_impl_->OnBackForwardCacheTimeout();
  back_forward_cache_timeout_monitor_.Stop();
}

void PageLifecycleStateManager::SetDelegateForTesting(
    PageLifecycleStateManager::TestDelegate* test_delegate) {
  CHECK(!test_delegate_ || !test_delegate, base::NotFatalUntil::M158);
  test_delegate_ = test_delegate;
}

std::ostream& operator<<(
    std::ostream& o,
    const PageLifecycleStateManager::BackForwardCacheEntered& s) {
  switch (s) {
    case PageLifecycleStateManager::BackForwardCacheEntered::kNo:
      return o << "kNo";
    case PageLifecycleStateManager::BackForwardCacheEntered::kEntering:
      return o << "kEntering";
    case PageLifecycleStateManager::BackForwardCacheEntered::kEntered:
      return o << "kEntered";
  }
}

}  // namespace content
