| // Copyright 2023 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/navigation_transitions/navigation_transition_utils.h" |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "components/viz/common/gpu/raster_context_provider.h" |
| #include "components/viz/host/host_frame_sink_manager.h" |
| #include "content/browser/compositor/surface_utils.h" |
| #include "content/browser/renderer_host/frame_tree.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/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_view_base.h" |
| #include "content/public/common/content_features.h" |
| #include "gpu/command_buffer/client/client_shared_image.h" |
| #include "ui/gfx/animation/animation.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "content/browser/renderer_host/compositor_impl_android.h" |
| #include "content/browser/renderer_host/render_widget_host_view_android.h" |
| #include "ui/android/view_android.h" |
| #include "ui/android/window_android.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| using CacheHitOrMissReason = NavigationTransitionData::CacheHitOrMissReason; |
| |
| static gfx::Size g_output_size_for_test = gfx::Size(); |
| |
| static int g_num_copy_requests_issued_for_testing = 0; |
| |
| // Construct a function-local variable instead of a standalone callback. |
| // Static local variables are first initialized when the function is first |
| // called (so we don't accidentally use this callback before it is even |
| // initialized); and base::NoDestructor makes sure the non-trivial destructor is |
| // not invoked. |
| NavigationTransitionUtils::ScreenshotCallback& GetTestScreenshotCallback() { |
| static base::NoDestructor<NavigationTransitionUtils::ScreenshotCallback> |
| instance; |
| return *instance; |
| } |
| |
| // Expect the following test methods to only be called if |
| // GetTestScreenshotCallback() is defined, and expect exactly one |
| // invocation for every call to CaptureNavigationEntryScreenshot. |
| // DO NOT invoke the test callback if the entry no longer exists. |
| void InvokeTestCallbackForNoScreenshot( |
| const NavigationRequest& navigation_request) { |
| if (!GetTestScreenshotCallback()) { |
| return; |
| } |
| |
| SkBitmap override_unused; |
| GetTestScreenshotCallback().Run(navigation_request.frame_tree_node() |
| ->navigator() |
| .controller() |
| .GetLastCommittedEntryIndex(), |
| {}, false, override_unused); |
| } |
| |
| void InvokeTestCallback(int index, |
| const SkBitmap bitmap, |
| bool requested, |
| SkBitmap& override_bitmap) { |
| SkBitmap test_copy(bitmap); |
| test_copy.setImmutable(); |
| GetTestScreenshotCallback().Run(index, test_copy, requested, override_bitmap); |
| } |
| |
| bool SupportsETC1NonPowerOfTwo(const NavigationRequest& navigation_request) { |
| #if BUILDFLAG(IS_ANDROID) |
| auto* rfh = navigation_request.frame_tree_node()->current_frame_host(); |
| auto* rwhv = rfh->GetView(); |
| auto* window_android = rwhv->GetNativeView()->GetWindowAndroid(); |
| auto* compositor = window_android->GetCompositor(); |
| return static_cast<CompositorImpl*>(compositor)->SupportsETC1NonPowerOfTwo(); |
| #else |
| return false; |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| // Returns the first entry that matches `destination_token`. Returns null if no |
| // match is found. |
| NavigationEntryImpl* GetEntryForToken( |
| NavigationControllerImpl* controller, |
| const blink::SameDocNavigationScreenshotDestinationToken& |
| destination_token) { |
| for (int i = 0; i < controller->GetEntryCount(); ++i) { |
| if (auto* entry = controller->GetEntryAtIndex(i); |
| entry->navigation_transition_data() |
| .same_document_navigation_entry_screenshot_token() == |
| destination_token) { |
| return entry; |
| } |
| } |
| return nullptr; |
| } |
| |
| void CacheScreenshotImpl(base::WeakPtr<NavigationControllerImpl> controller, |
| base::WeakPtr<NavigationRequest> navigation_request, |
| NavigationTransitionData::UniqueId screenshot_id, |
| bool is_copied_from_embedder, |
| int copy_output_request_sequence, |
| bool supports_etc_non_power_of_two, |
| const SkBitmap& bitmap) { |
| if (!controller) { |
| // The tab was destroyed by the time we receive the bitmap from the GPU. |
| return; |
| } |
| |
| int entry_index = |
| NavigationTransitionUtils::FindEntryIndexForNavigationTransitionID( |
| controller.get(), screenshot_id); |
| NavigationEntryImpl* entry = controller->GetEntryAtIndex(entry_index); |
| if (!entry || |
| entry->navigation_transition_data().copy_output_request_sequence() != |
| copy_output_request_sequence) { |
| // The entry has changed state since this request occurred so ignore it. |
| return; |
| } |
| |
| SkBitmap bitmap_copy(bitmap); |
| |
| if (GetTestScreenshotCallback()) { |
| SkBitmap override_bitmap; |
| InvokeTestCallback(entry_index, bitmap, true, override_bitmap); |
| if (!override_bitmap.drawsNothing()) { |
| bitmap_copy = override_bitmap; |
| } |
| } |
| |
| if (bitmap_copy.drawsNothing()) { |
| if (entry) { |
| entry->navigation_transition_data().set_cache_hit_or_miss_reason( |
| is_copied_from_embedder |
| ? CacheHitOrMissReason::kCapturedEmptyBitmapFromEmbedder |
| : CacheHitOrMissReason::kCapturedEmptyBitmapFromWebPage); |
| entry->navigation_transition_data().set_is_copied_from_embedder( |
| is_copied_from_embedder); |
| } |
| return; |
| } |
| |
| bitmap_copy.setImmutable(); |
| |
| auto screenshot = std::make_unique<NavigationEntryScreenshot>( |
| bitmap_copy, screenshot_id, supports_etc_non_power_of_two); |
| NavigationEntryScreenshotCache* cache = |
| controller->GetNavigationEntryScreenshotCache(); |
| cache->SetScreenshot(std::move(navigation_request), std::move(screenshot), |
| is_copied_from_embedder); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| void CacheScreenshotSharedImageImpl( |
| base::WeakPtr<NavigationControllerImpl> controller, |
| base::WeakPtr<NavigationRequest> navigation_request, |
| scoped_refptr<viz::RasterContextProvider> raster_context_provider, |
| NavigationTransitionData::UniqueId screenshot_id, |
| bool is_copied_from_embedder, |
| int copy_output_request_sequence, |
| bool supports_etc_non_power_of_two, |
| scoped_refptr<gpu::ClientSharedImage> shared_image) { |
| if (!controller) { |
| // The tab was destroyed by the time we receive the shared image from the |
| // GPU. |
| return; |
| } |
| TRACE_EVENT("content", "CacheScreenshotSharedImageImpl"); |
| |
| int entry_index = |
| NavigationTransitionUtils::FindEntryIndexForNavigationTransitionID( |
| controller.get(), screenshot_id); |
| NavigationEntryImpl* entry = controller->GetEntryAtIndex(entry_index); |
| if (!entry || |
| entry->navigation_transition_data().copy_output_request_sequence() != |
| copy_output_request_sequence) { |
| // The entry has changed state since this request occurred so ignore it. |
| return; |
| } |
| |
| // TODO(crbug.com/438496406): Figure out how to test shared images. |
| |
| if (!shared_image) { |
| if (entry) { |
| entry->navigation_transition_data().set_cache_hit_or_miss_reason( |
| is_copied_from_embedder |
| ? CacheHitOrMissReason::kCapturedEmptyBitmapFromEmbedder |
| : CacheHitOrMissReason::kCapturedEmptyBitmapFromWebPage); |
| entry->navigation_transition_data().set_is_copied_from_embedder( |
| is_copied_from_embedder); |
| } |
| return; |
| } |
| auto screenshot = std::make_unique<NavigationEntryScreenshot>( |
| std::move(shared_image), screenshot_id, supports_etc_non_power_of_two, |
| std::move(raster_context_provider)); |
| NavigationEntryScreenshotCache* cache = |
| controller->GetNavigationEntryScreenshotCache(); |
| cache->SetScreenshot(std::move(navigation_request), std::move(screenshot), |
| is_copied_from_embedder); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // We only want to capture screenshots for navigation entries reachable via |
| // session history navigations. Namely, we don't capture for navigations where |
| // the previous `NavigationEntry` will be either reloaded or replaced and |
| // deleted (e.g., `location.replace`, non-primary `FrameTree` navigations, etc). |
| bool CanTraverseToPreviousEntryAfterNavigation( |
| const NavigationRequest& navigation_request) { |
| if (navigation_request.GetReloadType() != ReloadType::NONE) { |
| // We don't capture for reloads. |
| return false; |
| } |
| |
| if (navigation_request.common_params().should_replace_current_entry) { |
| // If the `NavigationEntry` that's about to be committed will replace the |
| // previous `NavigationEntry`, we can't traverse to the previous |
| // `NavigationEntry` after that. |
| // This excludes the first navigation of a tab that replaces the initial |
| // `NavigationEntry`, since there is no page to go back to after the initial |
| // navigation. |
| return false; |
| } |
| |
| // Navigations in the non-primary `FrameTree` will always replace/reload, as |
| // they're guaranteed to only have a single entry for the session history. |
| CHECK(navigation_request.frame_tree_node()->frame_tree().is_primary()); |
| |
| return true; |
| } |
| |
| bool CanInitiateCaptureForNavigationStage( |
| const NavigationRequest& navigation_request, |
| bool did_receive_commit_ack) { |
| // We need to initiate the capture sooner for same-RFH navigations since |
| // the RFH switches to rendering the new Document as soon as the navigation |
| // commits in the renderer. |
| // TODO(khushalsagar): This can be removed after RenderDocument. |
| const bool is_same_render_frame_host = |
| navigation_request.frame_tree_node()->current_frame_host() == |
| navigation_request.GetRenderFrameHost(); |
| |
| if (is_same_render_frame_host) { |
| return !did_receive_commit_ack; |
| } |
| |
| return did_receive_commit_ack; |
| } |
| |
| // Purge any existing screenshots from the destination entry. Invalidate instead |
| // of overwriting here because the screenshot is stale and can't be used |
| // anymore in future navigations to this entry, as the document that's about to |
| // be loaded might have different contents than when the screenshot was taken in |
| // a previous load. A new screenshot should be taken when navigating away from |
| // this entry again. |
| void RemoveScreenshotFromDestination( |
| NavigationControllerImpl& navigation_controller, |
| NavigationEntry* destination_entry) { |
| if (!navigation_controller.frame_tree().is_primary()) { |
| // Navigations in the non-primary FrameTree can still have a destination |
| // entry (e.g., Prerender's initial document-fetch request will create a |
| // pending entry), but they won't have a screenshot because the non-primary |
| // FrameTree can't access the `NavigationEntryScreenshotCache`. |
| CHECK_EQ(navigation_controller.GetEntryCount(), 1); |
| CHECK(!navigation_controller.GetEntryAtIndex(0)->GetUserData( |
| NavigationEntryScreenshot::kUserDataKey)); |
| return; |
| } |
| |
| NavigationEntryScreenshotCache* cache = |
| navigation_controller.GetNavigationEntryScreenshotCache(); |
| if (destination_entry->GetUserData(NavigationEntryScreenshot::kUserDataKey)) { |
| std::unique_ptr<NavigationEntryScreenshot> successfully_removed = |
| cache->RemoveScreenshot(destination_entry); |
| CHECK(successfully_removed); |
| } |
| |
| // Also ensure that any existing in-flight CopyOutputRequests will be |
| // invalidated and their callbacks ignored. This ensures that new |
| // CopyOutputRequests can be made without interference / double-caching. |
| NavigationEntryImpl::FromNavigationEntry(destination_entry) |
| ->navigation_transition_data() |
| .increment_copy_output_request_sequence(); |
| } |
| |
| bool ShouldSkipScreenshotWithMissReason( |
| const NavigationRequest& navigation_request, |
| std::optional<CacheHitOrMissReason>& reason) { |
| if (gfx::Animation::PrefersReducedMotion()) { |
| reason = CacheHitOrMissReason::kCacheMissPrefersReducedMotion; |
| return true; |
| } |
| if (navigation_request.frame_tree_node() |
| ->GetParentOrOuterDocumentOrEmbedder()) { |
| // No support for embedded pages (including GuestView or fenced frames). |
| reason = CacheHitOrMissReason::kCacheMissEmbeddedPages; |
| return true; |
| } |
| if (!navigation_request.IsInPrimaryMainFrame()) { |
| // See crbug.com/40896219: We will present the fallback UX for navigations |
| // in the subframes. |
| reason = CacheHitOrMissReason::kCacheMissNonPrimaryMainFrame; |
| return true; |
| } |
| if (navigation_request.IsHistory() && |
| navigation_request.GetNavigationEntryOffset() < 0 && |
| !navigation_request.GetDelegate()->SupportsForwardTransitionAnimation()) { |
| reason = CacheHitOrMissReason::kForwardTransitionAnimationNotSupported; |
| return true; |
| } |
| if (!CanTraverseToPreviousEntryAfterNavigation(navigation_request)) { |
| // No reason because this entry will never be reachable. |
| return true; |
| } |
| return false; |
| } |
| |
| void AddCacheHitOrMissReason(NavigationEntryImpl* entry, |
| CacheHitOrMissReason reason) { |
| if (reason == CacheHitOrMissReason::kCacheMissNonPrimaryMainFrame && |
| entry->navigation_transition_data().cache_hit_or_miss_reason()) { |
| // Navigating to an entry can involve multiple NavigationRequests, including |
| // a same-document main frame navigation paired with a cross-document |
| // subframe navigation. Don't reset the reason if it was previously set by a |
| // main frame NavigationRequest. |
| reason = *entry->navigation_transition_data().cache_hit_or_miss_reason(); |
| } |
| entry->navigation_transition_data().set_cache_hit_or_miss_reason(reason); |
| } |
| |
| } // namespace |
| |
| void NavigationTransitionUtils::SetCapturedScreenshotSizeForTesting( |
| const gfx::Size& size) { |
| g_output_size_for_test = size; |
| } |
| |
| int NavigationTransitionUtils::GetNumCopyOutputRequestIssuedForTesting() { |
| return g_num_copy_requests_issued_for_testing; |
| } |
| |
| void NavigationTransitionUtils::ResetNumCopyOutputRequestIssuedForTesting() { |
| g_num_copy_requests_issued_for_testing = 0; |
| } |
| |
| void NavigationTransitionUtils::SetNavScreenshotCallbackForTesting( |
| ScreenshotCallback screenshot_callback) { |
| GetTestScreenshotCallback() = std::move(screenshot_callback); |
| } |
| |
| bool NavigationTransitionUtils:: |
| CaptureNavigationEntryScreenshotForCrossDocumentNavigations( |
| NavigationRequest& navigation_request, |
| bool did_receive_commit_ack) { |
| if (!NavigationTransitionConfig::AreBackForwardTransitionsEnabled()) { |
| return false; |
| } |
| |
| CHECK(!navigation_request.IsSameDocument()); |
| |
| if (!CanInitiateCaptureForNavigationStage(navigation_request, |
| did_receive_commit_ack)) { |
| return false; |
| } |
| |
| // The current conditions for whether to capture a screenshot depend on |
| // `NavigationRequest::GetRenderFrameHost()`, so for now we should only get |
| // here after the `RenderFrameHost` has been selected for a successful |
| // navigation. |
| CHECK(navigation_request.HasRenderFrameHost()); |
| |
| NavigationControllerImpl& navigation_controller = |
| navigation_request.frame_tree_node()->navigator().controller(); |
| |
| if (auto* destination_entry = navigation_request.GetNavigationEntry()) { |
| // Remove the screenshot from the destination before checking the |
| // conditions. We might not capture for this navigation due to some |
| // conditions, but the navigation still continues, for which we need to |
| // remove the screenshot from the destination entry. |
| RemoveScreenshotFromDestination(navigation_controller, destination_entry); |
| } else { |
| // We don't always have a destination entry (e.g., a new (non-history) |
| // subframe navigation). However we should still capture for navigations |
| // even without destination entries, as the screenshots are captured for the |
| // origin entries of the navigations. |
| } |
| |
| auto* last_committed_entry = navigation_controller.GetLastCommittedEntry(); |
| |
| std::optional<CacheHitOrMissReason> reason; |
| if (ShouldSkipScreenshotWithMissReason(navigation_request, reason)) { |
| if (reason) { |
| AddCacheHitOrMissReason(last_committed_entry, *reason); |
| } |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| return false; |
| } |
| |
| bool only_use_embedder_screenshot = false; |
| switch (navigation_request.early_render_frame_host_swap_type()) { |
| case NavigationRequest::EarlyRenderFrameHostSwapType::kNone: |
| break; |
| case NavigationRequest::EarlyRenderFrameHostSwapType::kCrashedFrame: |
| // If we're navigating away from a crashed frame, it's not possible to |
| // get a screenshot and fallback UI should be used instead. |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kNavigateAwayFromCrashedPage); |
| return false; |
| case NavigationRequest::EarlyRenderFrameHostSwapType::kInitialFrame: |
| // TODO(khushalsagar): Confirm whether this is needed for Chrome's NTP |
| // navigation. |
| only_use_embedder_screenshot = true; |
| break; |
| case NavigationRequest::EarlyRenderFrameHostSwapType::kNavigationTransition: |
| NOTREACHED(); |
| } |
| |
| RenderFrameHostImpl* current_rfh = |
| navigation_request.frame_tree_node()->current_frame_host(); |
| RenderWidgetHostView* rwhv = current_rfh->GetView(); |
| if (!rwhv) { |
| // The current frame is crashed but early swap didn't happen for this |
| // navigation. |
| CHECK(!current_rfh->IsRenderFrameLive()); |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kNavigateAwayFromCrashedPageNoEarlySwap); |
| return false; |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| if (!rwhv->GetNativeView() || !rwhv->GetNativeView()->GetWindowAndroid() || |
| !rwhv->GetNativeView()->GetWindowAndroid()->GetCompositor()) { |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kNoRootWindowOrCompositor); |
| return false; |
| } |
| #endif |
| |
| if (!rwhv->IsSurfaceAvailableForCopy()) { |
| // See https://crbug.com/368289857: If we hide the WebContents after a |
| // same-RFH navigation starts, we invalidate the `viz::LocalSurfaceID` |
| // and the browser UI will not be embedding a new ID when the navigation |
| // finishes (`WebContentsImpl::DidNavigateMainFramePreCommit()` and |
| // `RenderWidgetHostViewAndroid::DidNavigate()`). We won't be able to |
| // screenshot the page if we navigate the WebContents again before the UI |
| // embeds |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kBrowserNotEmbeddingValidSurfaceId); |
| return false; |
| } |
| |
| // https://crbug.com/369356401: It's possible to issue two CopyOutputRequests |
| // against the last committed entry. Bump the `copy_output_request_sequence()` |
| // to prevent double-caching the screenshot. |
| last_committed_entry->navigation_transition_data() |
| .increment_copy_output_request_sequence(); |
| int request_sequence = last_committed_entry->navigation_transition_data() |
| .copy_output_request_sequence(); |
| bool copied_via_delegate = |
| navigation_request.GetDelegate()->MaybeCopyContentAreaAsBitmap( |
| base::BindOnce( |
| &CacheScreenshotImpl, navigation_controller.GetWeakPtr(), |
| navigation_request.GetWeakPtr(), |
| last_committed_entry->navigation_transition_data().unique_id(), |
| /*is_copied_from_embedder=*/true, request_sequence, |
| SupportsETC1NonPowerOfTwo(navigation_request))); |
| |
| if (!copied_via_delegate && only_use_embedder_screenshot) { |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| } |
| |
| if (copied_via_delegate || only_use_embedder_screenshot) { |
| return false; |
| } |
| |
| // |
| // The browser is guaranteed to issue the screenshot request beyond this. |
| // |
| |
| // Without `SetOutputSizeForTest`, `g_output_size_for_test` is empty, |
| // meaning we will capture at full-size, unless specified by tests. |
| const gfx::Size output_size = g_output_size_for_test; |
| |
| #if BUILDFLAG(IS_ANDROID) |
| if (base::FeatureList::IsEnabled( |
| features::kBackForwardTransitionsCrossDocSharedImage)) { |
| auto* rwhva = static_cast<RenderWidgetHostViewAndroid*>(rwhv); |
| auto context_provider = rwhva->GetRasterContextProvider(); |
| if (!context_provider) { |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kNoRootWindowOrCompositor); |
| return false; |
| } |
| rwhva->CopySharedImageFromExactSurface( |
| /*src_rect=*/gfx::Rect(), output_size, |
| base::BindOnce( |
| &CacheScreenshotSharedImageImpl, navigation_controller.GetWeakPtr(), |
| navigation_request.GetWeakPtr(), context_provider, |
| last_committed_entry->navigation_transition_data().unique_id(), |
| /*is_copied_from_embedder=*/false, request_sequence, |
| SupportsETC1NonPowerOfTwo(navigation_request))); |
| } else { |
| static_cast<RenderWidgetHostViewBase*>(rwhv) |
| ->CopyFromExactSurfaceWithIpcDelay( |
| /*src_rect=*/gfx::Rect(), output_size, |
| base::BindOnce( |
| &CacheScreenshotImpl, navigation_controller.GetWeakPtr(), |
| navigation_request.GetWeakPtr(), |
| last_committed_entry->navigation_transition_data().unique_id(), |
| /*is_copied_from_embedder=*/false, request_sequence, |
| SupportsETC1NonPowerOfTwo(navigation_request)), |
| NavigationTransitionConfig::ScreenshotSendResultDelay()); |
| } |
| #else |
| static_cast<RenderWidgetHostViewBase*>(rwhv)->CopyFromExactSurface( |
| /*src_rect=*/gfx::Rect(), output_size, |
| base::BindOnce( |
| &CacheScreenshotImpl, navigation_controller.GetWeakPtr(), |
| navigation_request.GetWeakPtr(), |
| last_committed_entry->navigation_transition_data().unique_id(), |
| /*is_copied_from_embedder=*/false, request_sequence, |
| SupportsETC1NonPowerOfTwo(navigation_request))); |
| #endif |
| |
| ++g_num_copy_requests_issued_for_testing; |
| |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kSentScreenshotRequest); |
| |
| return true; |
| } |
| |
| void NavigationTransitionUtils::SetSameDocumentNavigationEntryScreenshotToken( |
| NavigationRequest& navigation_request, |
| std::optional<blink::SameDocNavigationScreenshotDestinationToken> |
| destination_token) { |
| if (!NavigationTransitionConfig::AreBackForwardTransitionsEnabled()) { |
| // The source of this call is from the renderer. We can't always trust the |
| // renderer thus fail safely. |
| return; |
| } |
| |
| CHECK(navigation_request.IsSameDocument()); |
| |
| NavigationControllerImpl& nav_controller = |
| navigation_request.frame_tree_node()->navigator().controller(); |
| if (auto* destination_entry = navigation_request.GetNavigationEntry()) { |
| RemoveScreenshotFromDestination(nav_controller, destination_entry); |
| } else { |
| // All renderer-initiated same-document navigations will not have a |
| // destination entry (see |
| // `NavigationRequest::CreateForSynchronousRendererCommit`). |
| } |
| |
| // If the renderer sends a token, it implies it issued a copy request for the |
| // pre-navigation state. |
| if (destination_token) { |
| ++g_num_copy_requests_issued_for_testing; |
| } |
| |
| auto* last_committed_entry = nav_controller.GetLastCommittedEntry(); |
| std::optional<CacheHitOrMissReason> reason; |
| if (ShouldSkipScreenshotWithMissReason(navigation_request, reason)) { |
| if (reason) { |
| AddCacheHitOrMissReason(last_committed_entry, *reason); |
| } |
| InvokeTestCallbackForNoScreenshot(navigation_request); |
| return; |
| } |
| |
| if (!destination_token) { |
| return; |
| } |
| |
| if (GetEntryForToken(&nav_controller, *destination_token)) { |
| // Again, can't always trust the renderer to send a non-duplicated token. |
| return; |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| RenderFrameHostImpl* current_rfh = |
| navigation_request.frame_tree_node()->current_frame_host(); |
| RenderWidgetHostView* rwhv = current_rfh->GetView(); |
| if (auto* window_android = rwhv->GetNativeView()->GetWindowAndroid(); |
| !window_android || !window_android->GetCompositor()) { |
| last_committed_entry->navigation_transition_data() |
| .set_cache_hit_or_miss_reason( |
| CacheHitOrMissReason::kNoRootWindowOrCompositor); |
| return; |
| } |
| #endif |
| |
| // NOTE: `destination_token` is to set on the last committed entry (the |
| // screenshot's destination), instead of the destination entry of this |
| // `navigation_request` (`navigation_request.GetNavigationEntry()`). |
| |
| // `blink::SameDocNavigationScreenshotDestinationToken` is guaranteed |
| // non-empty. |
| last_committed_entry->navigation_transition_data() |
| .SetSameDocumentNavigationEntryScreenshotToken(*destination_token); |
| |
| CHECK(GetHostFrameSinkManager()); |
| |
| // It is possible to issue two CopyOutputRequests against the last committed |
| // entry. This happens when a same-RFH navigation commits in the browser at |
| // the same time as a same-document navigation commits in the renderer. For |
| // example, |
| // 1. Browser has a navigation A->B. At ready to commit, browser sends a |
| // screenshot request for A. |
| // 2. Renderer commits a same-document navigation from A->A'. The renderer |
| // issues a copy request for A at the same time as sending the commit message. |
| // Bump the `copy_output_request_sequence()` to prevent double-caching the |
| // screenshot for A. |
| // |
| // TODO(https://crbug.com/372301997): We will miss caching a screenshot for A' |
| // in this case. Record that reason explicitly. |
| last_committed_entry->navigation_transition_data() |
| .increment_copy_output_request_sequence(); |
| int request_sequence = last_committed_entry->navigation_transition_data() |
| .copy_output_request_sequence(); |
| |
| GetHostFrameSinkManager()->SetOnCopyOutputReadyCallback( |
| *destination_token, |
| base::BindOnce( |
| &CacheScreenshotImpl, nav_controller.GetWeakPtr(), |
| navigation_request.GetWeakPtr(), |
| last_committed_entry->navigation_transition_data().unique_id(), |
| /*is_copied_from_embedder=*/false, request_sequence, |
| SupportsETC1NonPowerOfTwo(navigation_request))); |
| } |
| |
| int NavigationTransitionUtils::FindEntryIndexForNavigationTransitionID( |
| NavigationControllerImpl* controller, |
| NavigationTransitionData::UniqueId id) { |
| for (int i = 0; i < controller->GetEntryCount(); ++i) { |
| NavigationEntryImpl* entry = controller->GetEntryAtIndex(i); |
| if (entry->navigation_transition_data().unique_id() == id) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| bool NavigationTransitionUtils::ShouldSkipScreenshot( |
| const NavigationRequest& navigation_request) { |
| if (!base::FeatureList::IsEnabled(blink::features::kBackForwardTransitions)) { |
| // Preserve existing behavior, where the renderer decides. |
| return false; |
| } |
| std::optional<CacheHitOrMissReason> reason; |
| return ShouldSkipScreenshotWithMissReason(navigation_request, reason); |
| } |
| |
| } // namespace content |