| // Copyright 2019 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/sharing/shared_clipboard/remote_copy_message_handler.h" |
| |
| #include <algorithm> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/guid.h" |
| #include "base/numerics/ranges.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/task/thread_pool.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/notifications/notification_display_service.h" |
| #include "chrome/browser/notifications/notification_display_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sharing/proto/remote_copy_message.pb.h" |
| #include "chrome/browser/sharing/proto/sharing_message.pb.h" |
| #include "chrome/browser/sharing/shared_clipboard/feature_flags.h" |
| #include "chrome/browser/sharing/sharing_metrics.h" |
| #include "chrome/browser/sharing/sharing_service.h" |
| #include "chrome/browser/sharing/sharing_service_factory.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "net/base/load_flags.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "skia/ext/image_operations.h" |
| #include "ui/base/accelerators/accelerator.h" |
| #include "ui/base/clipboard/clipboard_buffer.h" |
| #include "ui/base/clipboard/scoped_clipboard_writer.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/time_format.h" |
| #include "ui/base/text/bytes_formatting.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "ui/message_center/public/cpp/notification_types.h" |
| #include "ui/message_center/public/cpp/notifier_id.h" |
| |
| #if defined(OS_WIN) |
| #include "chrome/browser/notifications/notification_platform_bridge_win.h" |
| #endif // defined(OS_WIN) |
| |
| namespace { |
| constexpr size_t kMaxImageDownloadSize = 5 * 1024 * 1024; |
| |
| // These values are the 2x of the preferred width and height defined in |
| // message_center_constants.h, which are in dip. |
| constexpr int kNotificationImageMaxWidthPx = 720; |
| constexpr int kNotificationImageMaxHeightPx = 480; |
| |
| // The initial delay for the timer that detects clipboard writes. An exponential |
| // backoff will double this value whenever the OneShotTimer reschedules. |
| constexpr base::TimeDelta kInitialDetectionTimerDelay = |
| base::TimeDelta::FromMilliseconds(1); |
| |
| // Interval at which to update the progress notification for image downloads. |
| constexpr base::TimeDelta kImageDownloadUpdateProgressInterval = |
| base::TimeDelta::FromMilliseconds(250); |
| |
| // This method should be called on a ThreadPool thread because it performs a |
| // potentially slow operation. |
| SkBitmap ResizeImage(const SkBitmap& image, int width, int height) { |
| TRACE_EVENT2("sharing", "ResizeImage", "src_pixels", |
| image.width() * image.height(), "dst_pixels", width * height); |
| return skia::ImageOperations::Resize( |
| image, skia::ImageOperations::RESIZE_BEST, width, height); |
| } |
| |
| const net::NetworkTrafficAnnotationTag kTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("remote_copy_message_handler", |
| R"( |
| semantics { |
| sender: "RemoteCopyMessageHandler" |
| description: |
| "Fetches an image from a URL specified in an FCM message." |
| trigger: |
| "The user sent an image to this device from another device that " |
| "they control." |
| data: |
| "An image URL, from a Google storage service like blobstore." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "Users can disable this behavior by signing out of Chrome." |
| policy_exception_justification: |
| "Can be controlled via Chrome sign-in." |
| })"); |
| |
| base::string16 GetTextNotificationTitle(const std::string& device_name) { |
| return device_name.empty() |
| ? l10n_util::GetStringUTF16( |
| IDS_SHARING_REMOTE_COPY_NOTIFICATION_TITLE_TEXT_CONTENT_UNKNOWN_DEVICE) |
| : l10n_util::GetStringFUTF16( |
| IDS_SHARING_REMOTE_COPY_NOTIFICATION_TITLE_TEXT_CONTENT, |
| base::UTF8ToUTF16(device_name)); |
| } |
| |
| base::string16 GetImageNotificationTitle(const std::string& device_name) { |
| return device_name.empty() |
| ? l10n_util::GetStringUTF16( |
| IDS_SHARING_REMOTE_COPY_NOTIFICATION_TITLE_IMAGE_CONTENT_UNKNOWN_DEVICE) |
| : l10n_util::GetStringFUTF16( |
| IDS_SHARING_REMOTE_COPY_NOTIFICATION_TITLE_IMAGE_CONTENT, |
| base::UTF8ToUTF16(device_name)); |
| } |
| |
| base::string16 GetRemainingTimeString(int64_t current, |
| int64_t total, |
| base::TimeDelta elapsed) { |
| if (total <= 0) |
| return base::string16(); |
| |
| int64_t elapsed_ms = elapsed.InMilliseconds(); |
| int64_t bytes_per_second = elapsed_ms == 0 ? 0 : current * 1000 / elapsed_ms; |
| int64_t remaining_bytes = total - current; |
| base::TimeDelta remaining_time = |
| base::TimeDelta::FromSeconds(remaining_bytes / bytes_per_second); |
| |
| return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING, |
| ui::TimeFormat::LENGTH_SHORT, remaining_time); |
| } |
| |
| base::string16 GetProgressString(int64_t current, int64_t total) { |
| ui::DataUnits amount_units = ui::GetByteDisplayUnits(total); |
| base::string16 current_string = |
| ui::FormatBytesWithUnits(current, amount_units, /*show_units=*/false); |
| base::string16 total_string = |
| ui::FormatBytesWithUnits(total, amount_units, /*show_units=*/true); |
| |
| return l10n_util::GetStringFUTF16(IDS_DOWNLOAD_STATUS_SIZES, current_string, |
| total_string); |
| } |
| |
| bool CanUpdateProgressNotification() { |
| #if defined(OS_WIN) |
| // TODO(crbug.com/1064558): Windows native notifications don't support updates |
| // so only show the initial progress notification and replace it with the |
| // final image notification at the end. |
| if (NotificationPlatformBridgeWin::NativeNotificationEnabled()) |
| return false; |
| #endif // defined(OS_WIN) |
| return true; |
| } |
| |
| } // namespace |
| |
| RemoteCopyMessageHandler::RemoteCopyMessageHandler(Profile* profile) |
| : profile_(profile) {} |
| |
| RemoteCopyMessageHandler::~RemoteCopyMessageHandler() = default; |
| |
| void RemoteCopyMessageHandler::OnMessage( |
| chrome_browser_sharing::SharingMessage message, |
| DoneCallback done_callback) { |
| DCHECK(message.has_remote_copy_message()); |
| TRACE_EVENT0("sharing", "RemoteCopyMessageHandler::OnMessage"); |
| |
| // First cancel any pending async tasks that might otherwise overwrite the |
| // results of the more recent message. |
| CancelAsyncTasks(); |
| |
| device_name_ = message.sender_device_name(); |
| |
| switch (message.remote_copy_message().content_case()) { |
| case chrome_browser_sharing::RemoteCopyMessage::kText: |
| HandleText(message.remote_copy_message().text()); |
| break; |
| case chrome_browser_sharing::RemoteCopyMessage::kImageUrl: |
| HandleImage(message.remote_copy_message().image_url()); |
| break; |
| case chrome_browser_sharing::RemoteCopyMessage::CONTENT_NOT_SET: |
| NOTREACHED(); |
| break; |
| } |
| |
| std::move(done_callback).Run(/*response=*/nullptr); |
| } |
| |
| void RemoteCopyMessageHandler::HandleText(const std::string& text) { |
| TRACE_EVENT1("sharing", "RemoteCopyMessageHandler::HandleText", "text_size", |
| text.size()); |
| |
| if (text.empty()) { |
| Finish(RemoteCopyHandleMessageResult::kFailureEmptyText); |
| return; |
| } |
| |
| LogRemoteCopyReceivedTextSize(text.size()); |
| |
| uint64_t old_sequence_number = |
| ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( |
| ui::ClipboardBuffer::kCopyPaste); |
| base::ElapsedTimer write_timer; |
| { |
| ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste) |
| .WriteText(base::UTF8ToUTF16(text)); |
| } |
| LogRemoteCopyWriteTime(write_timer.Elapsed(), /*is_image=*/false); |
| // Unretained(this) is safe here because |this| owns |write_detection_timer_|. |
| write_detection_timer_.Start( |
| FROM_HERE, kInitialDetectionTimerDelay, |
| base::BindOnce(&RemoteCopyMessageHandler::DetectWrite, |
| base::Unretained(this), old_sequence_number, |
| base::TimeTicks::Now(), /*is_image=*/false)); |
| notification_id_ = base::GenerateGUID(); |
| ShowNotification(GetTextNotificationTitle(device_name_), SkBitmap()); |
| Finish(RemoteCopyHandleMessageResult::kSuccessHandledText); |
| } |
| |
| void RemoteCopyMessageHandler::HandleImage(const std::string& image_url) { |
| TRACE_EVENT0("sharing", "RemoteCopyMessageHandler::HandleImage"); |
| |
| GURL url(image_url); |
| |
| if (!network::IsUrlPotentiallyTrustworthy(url)) { |
| Finish(RemoteCopyHandleMessageResult::kFailureImageUrlNotTrustworthy); |
| return; |
| } |
| |
| if (!IsImageSourceAllowed(url)) { |
| Finish(RemoteCopyHandleMessageResult::kFailureImageOriginNotAllowed); |
| return; |
| } |
| |
| bool should_show_progress = |
| base::FeatureList::IsEnabled(kRemoteCopyProgressNotification); |
| bool can_update_notification = CanUpdateProgressNotification(); |
| |
| if (should_show_progress) { |
| ClearProgressAndCloseNotification(); |
| UpdateProgressNotification(l10n_util::GetStringUTF16( |
| can_update_notification |
| ? IDS_SHARING_REMOTE_COPY_NOTIFICATION_PREPARING_DOWNLOAD |
| : IDS_SHARING_REMOTE_COPY_NOTIFICATION_PROCESSING_IMAGE)); |
| } |
| |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = url; |
| // This request should be unauthenticated (no cookies), and shouldn't be |
| // stored in the cache (this URL is only fetched once, ever.) |
| request->load_flags = net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE; |
| request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| |
| url_loader_ = |
| network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation); |
| timer_ = base::ElapsedTimer(); |
| // Unretained(this) is safe here because |this| owns |url_loader_|. |
| if (should_show_progress && can_update_notification) { |
| url_loader_->SetOnResponseStartedCallback( |
| base::BindOnce(&RemoteCopyMessageHandler::OnImageResponseStarted, |
| base::Unretained(this))); |
| url_loader_->SetOnDownloadProgressCallback( |
| base::BindRepeating(&RemoteCopyMessageHandler::OnImageDownloadProgress, |
| base::Unretained(this))); |
| } |
| url_loader_->DownloadToString( |
| content::BrowserContext::GetDefaultStoragePartition(profile_) |
| ->GetURLLoaderFactoryForBrowserProcess() |
| .get(), |
| base::BindOnce(&RemoteCopyMessageHandler::OnURLLoadComplete, |
| base::Unretained(this)), |
| kMaxImageDownloadSize); |
| } |
| |
| bool RemoteCopyMessageHandler::IsImageSourceAllowed(const GURL& image_url) { |
| std::vector<std::string> parts = |
| base::SplitString(kRemoteCopyAllowedOrigins.Get(), ",", |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| for (const auto& part : parts) { |
| GURL allowed_origin(part); |
| // The actual image URL may have a hash in the subdomain. This means we |
| // cannot match the entire host - we'll match the domain instead. |
| if (image_url.SchemeIs(allowed_origin.scheme_piece()) && |
| image_url.DomainIs(allowed_origin.host_piece()) && |
| image_url.EffectiveIntPort() == allowed_origin.EffectiveIntPort()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void RemoteCopyMessageHandler::OnImageResponseStarted( |
| const GURL& final_url, |
| const network::mojom::URLResponseHead& response_head) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| image_content_length_ = response_head.content_length; |
| } |
| |
| void RemoteCopyMessageHandler::OnImageDownloadProgress(uint64_t current) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| image_content_progress_ = current; |
| } |
| |
| void RemoteCopyMessageHandler::UpdateProgressNotification( |
| const base::string16& context) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (notification_id_.empty()) { |
| notification_id_ = base::GenerateGUID(); |
| // base::Unretained is safe as the SharingService owns |this| via the |
| // SharingHandlerRegistry and also the passed callback. |
| SharingServiceFactory::GetForBrowserContext(profile_) |
| ->SetNotificationActionHandler( |
| notification_id_, |
| base::BindRepeating( |
| &RemoteCopyMessageHandler::OnProgressNotificationAction, |
| base::Unretained(this))); |
| } |
| |
| message_center::RichNotificationData rich_notification_data; |
| rich_notification_data.vector_small_image = &kSendTabToSelfIcon; |
| rich_notification_data.never_timeout = true; |
| |
| message_center::Notification notification( |
| message_center::NOTIFICATION_TYPE_PROGRESS, notification_id_, |
| GetImageNotificationTitle(device_name_), |
| GetRemainingTimeString(image_content_progress_, image_content_length_, |
| timer_.Elapsed()), |
| /*icon=*/gfx::Image(), |
| /*display_source=*/base::string16(), |
| /*origin_url=*/GURL(), message_center::NotifierId(), |
| rich_notification_data, |
| /*delegate=*/nullptr); |
| |
| std::vector<message_center::ButtonInfo> notification_actions; |
| message_center::ButtonInfo button_info = |
| message_center::ButtonInfo(l10n_util::GetStringUTF16(IDS_CANCEL)); |
| notification_actions.push_back(button_info); |
| notification.set_buttons(notification_actions); |
| |
| if (image_content_length_ <= 0) { |
| // TODO(knollr): Show transfer status if |image_content_progress_| is != 0. |
| // This might happen if we don't know the total size of the image but we |
| // still want to show how many bytes have been transferred. |
| notification.set_progress(-1); |
| #if defined(OS_MACOSX) |
| // On macOS we only have the title and message available. The progress is |
| // prepended to the title and the message should be the context. |
| notification.set_message(context); |
| #else |
| notification.set_progress_status(context); |
| #endif // defined(OS_MACOSX) |
| } else { |
| notification.set_progress(image_content_progress_ * 100 / |
| image_content_length_); |
| base::string16 progress = |
| GetProgressString(image_content_progress_, image_content_length_); |
| #if defined(OS_MACOSX) |
| // On macOS we only have the title and message available. The progress is |
| // prepended to the title and the message should be the progress. |
| notification.set_message(progress); |
| #else |
| notification.set_progress_status(progress); |
| #endif // defined(OS_MACOSX) |
| } |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile_)->Display( |
| NotificationHandler::Type::SHARING, notification, /*metadata=*/nullptr); |
| |
| if (!CanUpdateProgressNotification()) |
| return; |
| |
| // Unretained(this) is safe here because |this| owns |
| // |image_download_update_progress_timer_|. |
| image_download_update_progress_timer_.Start( |
| FROM_HERE, kImageDownloadUpdateProgressInterval, |
| base::BindOnce(&RemoteCopyMessageHandler::UpdateProgressNotification, |
| base::Unretained(this), context)); |
| } |
| |
| void RemoteCopyMessageHandler::ClearProgressAndCloseNotification() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| image_content_length_ = -1; |
| image_content_progress_ = 0; |
| progress_notification_closed_ = false; |
| |
| if (notification_id_.empty()) |
| return; |
| |
| SharingServiceFactory::GetForBrowserContext(profile_) |
| ->SetNotificationActionHandler(notification_id_, base::NullCallback()); |
| NotificationDisplayServiceFactory::GetForProfile(profile_)->Close( |
| NotificationHandler::Type::SHARING, notification_id_); |
| |
| notification_id_.clear(); |
| } |
| |
| void RemoteCopyMessageHandler::OnProgressNotificationAction( |
| base::Optional<int> button, |
| bool closed) { |
| // Clicks on the progress notification body are ignored. |
| if (!closed && !button) |
| return; |
| |
| // Stop updating the progress notification. |
| image_download_update_progress_timer_.AbandonAndStop(); |
| |
| // Let the download continue if the notification was dismissed. |
| if (closed) { |
| // Remove the handler as this notification is now closed. |
| SharingServiceFactory::GetForBrowserContext(profile_) |
| ->SetNotificationActionHandler(notification_id_, base::NullCallback()); |
| progress_notification_closed_ = true; |
| return; |
| } |
| |
| // Cancel the download if the cancel button was pressed. |
| DCHECK_EQ(0, *button); |
| CancelAsyncTasks(); |
| } |
| |
| void RemoteCopyMessageHandler::OnURLLoadComplete( |
| std::unique_ptr<std::string> content) { |
| TRACE_EVENT0("sharing", "RemoteCopyMessageHandler::OnURLLoadComplete"); |
| |
| if (!progress_notification_closed_ && CanUpdateProgressNotification()) { |
| image_content_length_ = -1; |
| UpdateProgressNotification(l10n_util::GetStringUTF16( |
| IDS_SHARING_REMOTE_COPY_NOTIFICATION_PROCESSING_IMAGE)); |
| } |
| |
| image_download_update_progress_timer_.AbandonAndStop(); |
| |
| int code; |
| if (url_loader_->NetError() != net::OK) { |
| code = url_loader_->NetError(); |
| } else if (!url_loader_->ResponseInfo() || |
| !url_loader_->ResponseInfo()->headers) { |
| code = net::OK; |
| } else { |
| code = url_loader_->ResponseInfo()->headers->response_code(); |
| } |
| LogRemoteCopyLoadImageStatusCode(code); |
| |
| url_loader_.reset(); |
| if (!content || content->empty()) { |
| Finish(RemoteCopyHandleMessageResult::kFailureNoImageContentLoaded); |
| return; |
| } |
| |
| LogRemoteCopyLoadImageTime(timer_.Elapsed()); |
| LogRemoteCopyReceivedImageSizeBeforeDecode(content->size()); |
| |
| timer_ = base::ElapsedTimer(); |
| ImageDecoder::Start(this, *content); |
| } |
| |
| void RemoteCopyMessageHandler::OnImageDecoded(const SkBitmap& image) { |
| TRACE_EVENT0("sharing", "RemoteCopyMessageHandler::OnImageDecoded"); |
| |
| if (image.drawsNothing()) { |
| Finish(RemoteCopyHandleMessageResult::kFailureDecodedImageDrawsNothing); |
| return; |
| } |
| |
| LogRemoteCopyDecodeImageTime(timer_.Elapsed()); |
| LogRemoteCopyReceivedImageSizeAfterDecode(image.computeByteSize()); |
| |
| if (!base::FeatureList::IsEnabled(kRemoteCopyImageNotification)) { |
| WriteImageAndShowNotification(image, image); |
| return; |
| } |
| |
| double scale = std::min( |
| static_cast<double>(kNotificationImageMaxWidthPx) / image.width(), |
| static_cast<double>(kNotificationImageMaxHeightPx) / image.height()); |
| |
| // If the image is too large to show in a notification, resize it first. |
| if (scale < 1.0) { |
| int resized_width = |
| base::ClampToRange(static_cast<int>(scale * image.width()), 0, |
| kNotificationImageMaxWidthPx); |
| int resized_height = |
| base::ClampToRange(static_cast<int>(scale * image.height()), 0, |
| kNotificationImageMaxHeightPx); |
| |
| // Unretained(this) is safe here because |this| owns |resize_callback_|. |
| resize_callback_.Reset( |
| base::BindOnce(&RemoteCopyMessageHandler::WriteImageAndShowNotification, |
| base::Unretained(this), image)); |
| timer_ = base::ElapsedTimer(); |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::USER_VISIBLE}, |
| base::BindOnce(&ResizeImage, image, resized_width, resized_height), |
| resize_callback_.callback()); |
| } else { |
| WriteImageAndShowNotification(image, image); |
| } |
| } |
| |
| void RemoteCopyMessageHandler::OnDecodeImageFailed() { |
| Finish(RemoteCopyHandleMessageResult::kFailureDecodeImageFailed); |
| } |
| |
| void RemoteCopyMessageHandler::WriteImageAndShowNotification( |
| const SkBitmap& original_image, |
| const SkBitmap& resized_image) { |
| TRACE_EVENT1("sharing", |
| "RemoteCopyMessageHandler::WriteImageAndShowNotification", |
| "bytes", original_image.computeByteSize()); |
| |
| if (original_image.dimensions() != resized_image.dimensions()) |
| LogRemoteCopyResizeImageTime(timer_.Elapsed()); |
| |
| uint64_t old_sequence_number = |
| ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( |
| ui::ClipboardBuffer::kCopyPaste); |
| base::ElapsedTimer write_timer; |
| { |
| ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste) |
| .WriteImage(original_image); |
| } |
| LogRemoteCopyWriteTime(write_timer.Elapsed(), /*is_image=*/true); |
| // Unretained(this) is safe here because |this| owns |write_detection_timer_|. |
| write_detection_timer_.Start( |
| FROM_HERE, kInitialDetectionTimerDelay, |
| base::BindOnce(&RemoteCopyMessageHandler::DetectWrite, |
| base::Unretained(this), old_sequence_number, |
| base::TimeTicks::Now(), /*is_image=*/true)); |
| |
| #if defined(OS_MACOSX) |
| // On macOS we can't replace a persistent notification with a non-persistent |
| // one because they are posted from different sources (app vs xpc). To avoid |
| // having both notifications on screen, remove the progress one first. |
| if (!progress_notification_closed_ && |
| !base::FeatureList::IsEnabled(kRemoteCopyPersistentNotification)) { |
| ClearProgressAndCloseNotification(); |
| } |
| #endif // defined(OS_MACOSX) |
| |
| // If the notification id is not empty there must be a progress notification |
| // that can be updated. Just clear its action handler. |
| if (!notification_id_.empty()) { |
| SharingServiceFactory::GetForBrowserContext(profile_) |
| ->SetNotificationActionHandler(notification_id_, base::NullCallback()); |
| } else { |
| notification_id_ = base::GenerateGUID(); |
| } |
| |
| ShowNotification(GetImageNotificationTitle(device_name_), resized_image); |
| Finish(RemoteCopyHandleMessageResult::kSuccessHandledImage); |
| } |
| |
| void RemoteCopyMessageHandler::ShowNotification(const base::string16& title, |
| const SkBitmap& image) { |
| TRACE_EVENT0("sharing", "RemoteCopyMessageHandler::ShowNotification"); |
| |
| gfx::Image icon; |
| message_center::RichNotificationData rich_notification_data; |
| |
| bool use_image_notification = |
| base::FeatureList::IsEnabled(kRemoteCopyImageNotification) && |
| !image.drawsNothing(); |
| |
| if (use_image_notification) { |
| #if defined(OS_MACOSX) |
| // On macOS notifications do not support large images so use the icon |
| // instead. |
| icon = gfx::Image::CreateFrom1xBitmap(image); |
| #else |
| rich_notification_data.image = gfx::Image::CreateFrom1xBitmap(image); |
| #endif // defined(OS_MACOSX) |
| } |
| |
| rich_notification_data.vector_small_image = &kSendTabToSelfIcon; |
| rich_notification_data.never_timeout = |
| base::FeatureList::IsEnabled(kRemoteCopyPersistentNotification); |
| |
| message_center::NotificationType type = |
| use_image_notification ? message_center::NOTIFICATION_TYPE_IMAGE |
| : message_center::NOTIFICATION_TYPE_SIMPLE; |
| |
| ui::Accelerator paste_accelerator(ui::VKEY_V, ui::EF_PLATFORM_ACCELERATOR); |
| |
| message_center::Notification notification( |
| type, notification_id_, title, |
| l10n_util::GetStringFUTF16( |
| IDS_SHARING_REMOTE_COPY_NOTIFICATION_DESCRIPTION, |
| paste_accelerator.GetShortcutText()), |
| icon, |
| /*display_source=*/base::string16(), |
| /*origin_url=*/GURL(), message_center::NotifierId(), |
| rich_notification_data, |
| /*delegate=*/nullptr); |
| |
| if (!CanUpdateProgressNotification()) |
| notification.set_renotify(true); |
| |
| // Make the notification silent if we're replacing a progress notification. |
| bool should_show_progress = |
| base::FeatureList::IsEnabled(kRemoteCopyProgressNotification); |
| if (should_show_progress && !progress_notification_closed_) |
| notification.set_silent(true); |
| |
| NotificationDisplayServiceFactory::GetForProfile(profile_)->Display( |
| NotificationHandler::Type::SHARING, notification, /*metadata=*/nullptr); |
| } |
| |
| void RemoteCopyMessageHandler::DetectWrite(uint64_t old_sequence_number, |
| base::TimeTicks start_ticks, |
| bool is_image) { |
| TRACE_EVENT0("sharing", "RemoteCopyMessageHandler::DetectWrite"); |
| |
| uint64_t current_sequence_number = |
| ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( |
| ui::ClipboardBuffer::kCopyPaste); |
| base::TimeDelta elapsed = base::TimeTicks::Now() - start_ticks; |
| if (current_sequence_number != old_sequence_number) { |
| LogRemoteCopyWriteDetectionTime(elapsed, is_image); |
| return; |
| } |
| |
| if (elapsed > base::TimeDelta::FromSeconds(10)) |
| return; |
| |
| // Unretained(this) is safe here because |this| owns |write_detection_timer_|. |
| base::TimeDelta backoff_delay = write_detection_timer_.GetCurrentDelay() * 2; |
| write_detection_timer_.Start( |
| FROM_HERE, backoff_delay, |
| base::BindOnce(&RemoteCopyMessageHandler::DetectWrite, |
| base::Unretained(this), old_sequence_number, start_ticks, |
| is_image)); |
| } |
| |
| void RemoteCopyMessageHandler::Finish(RemoteCopyHandleMessageResult result) { |
| TRACE_EVENT1("sharing", "RemoteCopyMessageHandler::Finish", "result", result); |
| |
| if (result != RemoteCopyHandleMessageResult::kSuccessHandledText && |
| result != RemoteCopyHandleMessageResult::kSuccessHandledImage) { |
| ClearProgressAndCloseNotification(); |
| } |
| |
| LogRemoteCopyHandleMessageResult(result); |
| device_name_.clear(); |
| } |
| |
| void RemoteCopyMessageHandler::CancelAsyncTasks() { |
| url_loader_.reset(); |
| ImageDecoder::Cancel(this); |
| resize_callback_.Cancel(); |
| write_detection_timer_.AbandonAndStop(); |
| image_download_update_progress_timer_.AbandonAndStop(); |
| ClearProgressAndCloseNotification(); |
| } |