| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/ash/clipboard_image_model_request.h" |
| |
| #include <memory> |
| |
| #include "ash/public/cpp/clipboard_history_controller.h" |
| #include "ash/public/cpp/scoped_clipboard_history_pause.h" |
| #include "base/base64.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/clipboard/clipboard_data.h" |
| #include "ui/base/clipboard/clipboard_non_backed.h" |
| #include "ui/base/data_transfer_policy/data_transfer_endpoint.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/widget/widget.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| // The maximum size that the web contents can be. It caps the memory consumption |
| // incurred by web contents rendering. |
| constexpr gfx::Size kMaxWebContentsSize(2000, 2000); |
| |
| // The initial size of the NativeView to force painting in an inactive shown |
| // widget for auto-resize mode. |
| constexpr gfx::Size kAutoResizeModeInitialSize(1, 1); |
| |
| ClipboardImageModelRequest::TestParams* g_test_params = nullptr; |
| |
| } // namespace |
| |
| // ClipboardImageModelFactory::Params: ----------------------------------------- |
| |
| ClipboardImageModelRequest::Params::Params(const base::UnguessableToken& id, |
| const std::string& html_markup, |
| const gfx::Size& bounding_box_size, |
| ImageModelCallback callback) |
| : id(id), |
| html_markup(html_markup), |
| bounding_box_size(bounding_box_size), |
| callback(std::move(callback)) {} |
| |
| ClipboardImageModelRequest::Params::Params(Params&&) = default; |
| |
| ClipboardImageModelRequest::Params& |
| ClipboardImageModelRequest::Params::operator=(Params&&) = default; |
| |
| ClipboardImageModelRequest::Params::~Params() = default; |
| |
| // ClipboardImageModelFactory::TestParams: ------------------------------------- |
| |
| ClipboardImageModelRequest::TestParams::TestParams(RequestStopCallback callback, |
| bool enforce_auto_resize) |
| : callback(callback), enforce_auto_resize(enforce_auto_resize) {} |
| |
| ClipboardImageModelRequest::TestParams::~TestParams() = default; |
| |
| // ClipboardImageModelRequest------------- ------------------------------------- |
| |
| ClipboardImageModelRequest::ScopedClipboardModifier::ScopedClipboardModifier( |
| const std::string& html_markup) { |
| auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread(); |
| ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory); |
| const auto* current_data = clipboard->GetClipboardData(&data_dst); |
| |
| // No need to replace the clipboard contents if the markup is the same. |
| if (current_data && (html_markup == current_data->markup_data())) |
| return; |
| |
| // Put |html_markup| on the clipboard temporarily so it can be pasted into |
| // the WebContents. This is preferable to directly loading |html_markup_| in a |
| // data URL because pasting the data into WebContents sanitizes the markup. |
| // TODO(https://crbug.com/1144962): Sanitize copied HTML prior to storing it |
| // in the clipboard buffer. Then |html_markup_| can be loaded from a data URL |
| // and will not need to be pasted in this manner. |
| auto new_data = std::make_unique<ui::ClipboardData>(); |
| new_data->set_markup_data(html_markup); |
| |
| scoped_clipboard_history_pause_ = |
| ash::ClipboardHistoryController::Get()->CreateScopedPause(); |
| replaced_clipboard_data_ = clipboard->WriteClipboardData(std::move(new_data)); |
| } |
| |
| ClipboardImageModelRequest::ScopedClipboardModifier:: |
| ~ScopedClipboardModifier() { |
| if (!replaced_clipboard_data_) |
| return; |
| |
| ui::ClipboardNonBacked::GetForCurrentThread()->WriteClipboardData( |
| std::move(replaced_clipboard_data_)); |
| } |
| |
| ClipboardImageModelRequest::ClipboardImageModelRequest( |
| Profile* profile, |
| base::RepeatingClosure on_request_finished_callback) |
| : widget_(std::make_unique<views::Widget>()), |
| web_view_(new views::WebView(profile)), |
| on_request_finished_callback_(std::move(on_request_finished_callback)), |
| request_creation_time_(base::TimeTicks::Now()) { |
| views::Widget::InitParams widget_params; |
| widget_params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS; |
| widget_params.ownership = |
| views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| widget_params.name = "ClipboardImageModelRequest"; |
| widget_->Init(std::move(widget_params)); |
| widget_->SetContentsView(web_view_); |
| |
| Observe(web_view_->GetWebContents()); |
| web_contents()->SetDelegate(this); |
| } |
| |
| ClipboardImageModelRequest::~ClipboardImageModelRequest() { |
| UMA_HISTOGRAM_TIMES("Ash.ClipboardHistory.ImageModelRequest.Lifetime", |
| base::TimeTicks::Now() - request_creation_time_); |
| } |
| |
| void ClipboardImageModelRequest::Start(Params&& params) { |
| DCHECK(!deliver_image_model_callback_); |
| DCHECK(params.callback); |
| DCHECK_EQ(base::UnguessableToken(), request_id_); |
| |
| request_id_ = std::move(params.id); |
| html_markup_ = params.html_markup; |
| deliver_image_model_callback_ = std::move(params.callback); |
| |
| timeout_timer_.Start(FROM_HERE, base::Seconds(10), this, |
| &ClipboardImageModelRequest::OnTimeout); |
| request_start_time_ = base::TimeTicks::Now(); |
| |
| // Begin the document with the proper charset, this should prevent strange |
| // looking characters from showing up in the render in some cases. |
| std::string html_document( |
| "<!DOCTYPE html>" |
| "<html>" |
| " <head><meta charset=\"UTF-8\"></meta></head>" |
| " <body contenteditable='true' style=\"overflow: hidden\"> " |
| " <script>" |
| // Focus the Contenteditable body to ensure WebContents::Paste() reaches |
| // the body. |
| " document.body.focus();" |
| " </script>" |
| " </body>" |
| "</html"); |
| |
| std::string encoded_html; |
| base::Base64Encode(html_document, &encoded_html); |
| constexpr char kDataURIPrefix[] = "data:text/html;base64,"; |
| web_contents()->GetController().LoadURLWithParams( |
| content::NavigationController::LoadURLParams( |
| GURL(kDataURIPrefix + encoded_html))); |
| widget_->ShowInactive(); |
| |
| // Adapt to the render widget host view whose device scale factor is not one. |
| bounding_box_size_ = gfx::ScaleToCeiledSize( |
| params.bounding_box_size, |
| web_contents()->GetRenderWidgetHostView()->GetDeviceScaleFactor()); |
| } |
| |
| void ClipboardImageModelRequest::Stop(RequestStopReason stop_reason) { |
| UMA_HISTOGRAM_ENUMERATION("Ash.ClipboardHistory.ImageModelRequest.StopReason", |
| stop_reason); |
| DCHECK(!request_start_time_.is_null()); |
| UMA_HISTOGRAM_TIMES("Ash.ClipboardHistory.ImageModelRequest.Runtime", |
| base::TimeTicks::Now() - request_start_time_); |
| request_start_time_ = base::TimeTicks(); |
| scoped_clipboard_modifier_.reset(); |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| copy_surface_weak_ptr_factory_.InvalidateWeakPtrs(); |
| timeout_timer_.Stop(); |
| widget_->Hide(); |
| deliver_image_model_callback_.Reset(); |
| request_id_ = base::UnguessableToken(); |
| did_stop_loading_ = false; |
| |
| on_request_finished_callback_.Run(); |
| |
| if (g_test_params && g_test_params->callback) |
| g_test_params->callback.Run(ShouldEnableAutoResizeMode()); |
| } |
| |
| ClipboardImageModelRequest::Params |
| ClipboardImageModelRequest::StopAndGetParams() { |
| DCHECK(IsRunningRequest()); |
| Params params(request_id_, html_markup_, bounding_box_size_, |
| std::move(deliver_image_model_callback_)); |
| Stop(RequestStopReason::kRequestCanceled); |
| return params; |
| } |
| |
| bool ClipboardImageModelRequest::IsModifyingClipboard() const { |
| return scoped_clipboard_modifier_.has_value(); |
| } |
| |
| bool ClipboardImageModelRequest::IsRunningRequest( |
| absl::optional<base::UnguessableToken> request_id) const { |
| return request_id.has_value() ? *request_id == request_id_ |
| : !request_id_.is_empty(); |
| } |
| |
| void ClipboardImageModelRequest::ResizeDueToAutoResize( |
| content::WebContents* web_contents, |
| const gfx::Size& new_size) { |
| web_contents->GetNativeView()->SetBounds(gfx::Rect(gfx::Point(), new_size)); |
| |
| // `ResizeDueToAutoResize()` can be called before and/or after |
| // DidStopLoading(). If `DidStopLoading()` has not been called, wait for the |
| // next resize before copying the surface. |
| if (!web_contents->IsLoading()) |
| PostCopySurfaceTask(); |
| } |
| |
| void ClipboardImageModelRequest::DidStopLoading() { |
| // `DidStopLoading()` can be called multiple times after a paste. We are only |
| // interested in the initial load of the data URL. |
| if (did_stop_loading_) |
| return; |
| |
| did_stop_loading_ = true; |
| |
| // Modify the clipboard so `html_markup_` can be pasted into the WebContents. |
| scoped_clipboard_modifier_.emplace(html_markup_); |
| |
| web_contents()->GetRenderViewHost()->GetWidget()->InsertVisualStateCallback( |
| base::BindOnce(&ClipboardImageModelRequest::OnVisualStateChangeFinished, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // After navigating to a new page, the surface id is invalidated. As a result, |
| // copy from surface is disabled as well. Setting the window's bounds should |
| // generate a new local surface id. Hence, window bounds setting should be |
| // after the web navigation. |
| // Changing auto resize mode does not generate a new local surface id while |
| // setting the window bounds will. As a result, enabling/disabling the auto |
| // resize mode has to precede the window bounds setting. Otherwise the change |
| // in the window bounds may trigger the unnecessary update in the view layout |
| // for the obsolete auto resize state. This layout update will consume the |
| // newly generated local surface id. Then it will cause a crash when the |
| // layout update brought by the change in the auto resize state arrives. |
| if (ShouldEnableAutoResizeMode()) { |
| web_contents()->GetRenderWidgetHostView()->EnableAutoResize( |
| kAutoResizeModeInitialSize, kMaxWebContentsSize); |
| web_contents()->GetNativeView()->SetBounds( |
| gfx::Rect(kAutoResizeModeInitialSize)); |
| } else { |
| web_contents()->GetRenderWidgetHostView()->DisableAutoResize( |
| bounding_box_size_); |
| web_contents()->GetNativeView()->SetBounds(gfx::Rect(bounding_box_size_)); |
| } |
| |
| // TODO(https://crbug.com/1149556): Clipboard Contents could be overwritten |
| // prior to the `WebContents::Paste()` completing. |
| web_contents()->Paste(); |
| } |
| |
| // static |
| void ClipboardImageModelRequest::SetTestParams(TestParams* test_params) { |
| // Supports only setting `g_test_params` or resetting it. |
| DCHECK(!g_test_params || !test_params); |
| g_test_params = test_params; |
| } |
| |
| void ClipboardImageModelRequest::OnVisualStateChangeFinished(bool done) { |
| if (!done) |
| return; |
| |
| scoped_clipboard_modifier_.reset(); |
| PostCopySurfaceTask(); |
| } |
| |
| void ClipboardImageModelRequest::PostCopySurfaceTask() { |
| if (!deliver_image_model_callback_) |
| return; |
| |
| // Debounce calls to `CopySurface()`. `DidStopLoading()` and |
| // `ResizeDueToAutoResize()` can be called multiple times in the same task |
| // sequence. Wait for the final update before copying the surface. |
| copy_surface_weak_ptr_factory_.InvalidateWeakPtrs(); |
| DCHECK( |
| web_contents()->GetRenderWidgetHostView()->IsSurfaceAvailableForCopy()); |
| base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&ClipboardImageModelRequest::CopySurface, |
| copy_surface_weak_ptr_factory_.GetWeakPtr()), |
| base::Milliseconds(250)); |
| } |
| |
| void ClipboardImageModelRequest::CopySurface() { |
| content::RenderWidgetHostView* source_view = |
| web_contents()->GetRenderViewHost()->GetWidget()->GetView(); |
| if (source_view->GetViewBounds().size().IsEmpty()) { |
| Stop(RequestStopReason::kEmptyResult); |
| return; |
| } |
| |
| // There is no guarantee CopyFromSurface will call OnCopyComplete. If this |
| // takes too long, this will be cleaned up by |timeout_timer_|. |
| source_view->CopyFromSurface( |
| /*src_rect=*/gfx::Rect(), /*output_size=*/gfx::Size(), |
| base::BindOnce(&ClipboardImageModelRequest::OnCopyComplete, |
| weak_ptr_factory_.GetWeakPtr(), |
| source_view->GetDeviceScaleFactor())); |
| } |
| |
| void ClipboardImageModelRequest::OnCopyComplete(float device_scale_factor, |
| const SkBitmap& bitmap) { |
| if (!deliver_image_model_callback_) { |
| Stop(RequestStopReason::kMultipleCopyCompletion); |
| return; |
| } |
| |
| std::move(deliver_image_model_callback_) |
| .Run(ui::ImageModel::FromImageSkia( |
| gfx::ImageSkia(gfx::ImageSkiaRep(bitmap, device_scale_factor)))); |
| Stop(RequestStopReason::kFulfilled); |
| } |
| |
| void ClipboardImageModelRequest::OnTimeout() { |
| DCHECK(deliver_image_model_callback_); |
| Stop(RequestStopReason::kTimeout); |
| } |
| |
| bool ClipboardImageModelRequest::ShouldEnableAutoResizeMode() const { |
| if (g_test_params) |
| return g_test_params->enforce_auto_resize; |
| |
| // Use auto resize mode if `bounding_box_size_` is not meaningful. |
| if (bounding_box_size_.IsEmpty()) |
| return true; |
| |
| // Use auto resize mode if the copied web content is too big to render. |
| return !gfx::Rect(kMaxWebContentsSize) |
| .Contains(gfx::Rect(bounding_box_size_)); |
| } |