| // 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 "chrome/browser/nearby_sharing/nearby_notification_manager.h" |
| |
| #include <string> |
| |
| #include "ash/constants/notifier_catalogs.h" |
| #include "ash/public/cpp/holding_space/holding_space_item.h" |
| #include "ash/public/cpp/notification_utils.h" |
| #include "ash/webui/settings/public/constants/routes.mojom.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/notreached.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "build/branding_buildflags.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/download/download_prefs.h" |
| #include "chrome/browser/image_decoder/image_decoder.h" |
| #include "chrome/browser/nearby_sharing/common/nearby_share_features.h" |
| #include "chrome/browser/nearby_sharing/common/nearby_share_prefs.h" |
| #include "chrome/browser/nearby_sharing/common/nearby_share_resource_getter.h" |
| #include "chrome/browser/nearby_sharing/nearby_share_metrics.h" |
| #include "chrome/browser/nearby_sharing/nearby_sharing_service.h" |
| #include "chrome/browser/notifications/notification_display_service.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service.h" |
| #include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h" |
| #include "chrome/browser/ui/settings_window_manager_chromeos.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chromeos/ash/services/nearby/public/mojom/nearby_share_settings.mojom.h" |
| #include "components/cross_device/logging/logging.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/download_manager.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/chromeos/styles/cros_tokens_color_mappings.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "ui/message_center/public/cpp/notification_types.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| #include "chrome/browser/nearby_sharing/internal/icons/vector_icons.h" |
| #endif // BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| |
| namespace { |
| |
| constexpr char kNearbyInProgressNotificationId[] = |
| "chrome://nearby_share/in_progress"; |
| constexpr char kNearbyTransferResultNotificationIdPrefix[] = |
| "chrome://nearby_share/result/"; |
| constexpr char kNearbyDeviceTryingToShareNotificationId[] = |
| "chrome://nearby_share/nearby_device_trying_to_share"; |
| constexpr char kNearbyVisibilityReminderNotificationId[] = |
| "chrome://nearby_share/visibility_reminder"; |
| constexpr char kNearbyNotifier[] = "nearby"; |
| |
| std::string CreateNotificationIdForShareTarget( |
| const ShareTarget& share_target) { |
| return std::string(kNearbyTransferResultNotificationIdPrefix) + |
| share_target.id.ToString(); |
| } |
| |
| // Creates a default Nearby Share notification with empty content. |
| message_center::Notification CreateNearbyNotification(const std::string& id) { |
| message_center::Notification notification( |
| message_center::NOTIFICATION_TYPE_SIMPLE, id, |
| /*title=*/std::u16string(), |
| /*message=*/std::u16string(), |
| /*icon=*/ui::ImageModel(), |
| features::IsNameEnabled() |
| ? NearbyShareResourceGetter::GetInstance()->GetStringWithFeatureName( |
| IDS_NEARBY_NOTIFICATION_SOURCE_PH) |
| : l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_SOURCE), |
| /*origin_url=*/GURL(), |
| message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT, |
| kNearbyNotifier, |
| ash::NotificationCatalogName::kNearbyShare), |
| /*optional_fields=*/{}, |
| /*delegate=*/nullptr); |
| |
| notification.set_accent_color_id(cros_tokens::kCrosSysPrimary); |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| if (features::IsNameEnabled()) { |
| notification.set_vector_small_image(kNearbyShareInternalIcon); |
| } else { |
| notification.set_vector_small_image(kNearbyShareIcon); |
| } |
| #else // !BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| notification.set_vector_small_image(kNearbyShareIcon); |
| #endif // !BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| |
| notification.set_settings_button_handler( |
| message_center::SettingsButtonHandler::DELEGATE); |
| |
| return notification; |
| } |
| |
| std::string GetTimestampString() { |
| return base::NumberToString( |
| base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| } |
| |
| FileAttachment::Type GetCommonFileAttachmentType( |
| const std::vector<FileAttachment>& files) { |
| if (files.empty()) |
| return FileAttachment::Type::kUnknown; |
| |
| FileAttachment::Type type = files[0].type(); |
| for (size_t i = 1; i < files.size(); ++i) { |
| if (files[i].type() != type) |
| return FileAttachment::Type::kUnknown; |
| } |
| return type; |
| } |
| |
| TextAttachment::Type GetCommonTextAttachmentType( |
| const std::vector<TextAttachment>& texts) { |
| if (texts.empty()) |
| return TextAttachment::Type::kText; |
| |
| TextAttachment::Type type = texts[0].type(); |
| for (size_t i = 1; i < texts.size(); ++i) { |
| if (texts[i].type() != type) |
| return TextAttachment::Type::kText; |
| } |
| return type; |
| } |
| |
| int GetFileAttachmentsCapitalizedStringId( |
| const std::vector<FileAttachment>& files) { |
| switch (GetCommonFileAttachmentType(files)) { |
| case FileAttachment::Type::kApp: |
| return IDS_NEARBY_FILE_ATTACHMENTS_CAPITALIZED_APPS; |
| case FileAttachment::Type::kImage: |
| return IDS_NEARBY_FILE_ATTACHMENTS_CAPITALIZED_IMAGES; |
| case FileAttachment::Type::kUnknown: |
| return IDS_NEARBY_FILE_ATTACHMENTS_CAPITALIZED_UNKNOWN; |
| case FileAttachment::Type::kVideo: |
| return IDS_NEARBY_FILE_ATTACHMENTS_CAPITALIZED_VIDEOS; |
| default: |
| return IDS_NEARBY_CAPITALIZED_UNKNOWN_ATTACHMENTS; |
| } |
| } |
| |
| int GetFileAttachmentsNotCapitalizedStringId( |
| const std::vector<FileAttachment>& files) { |
| switch (GetCommonFileAttachmentType(files)) { |
| case FileAttachment::Type::kApp: |
| return IDS_NEARBY_FILE_ATTACHMENTS_NOT_CAPITALIZED_APPS; |
| case FileAttachment::Type::kImage: |
| return IDS_NEARBY_FILE_ATTACHMENTS_NOT_CAPITALIZED_IMAGES; |
| case FileAttachment::Type::kUnknown: |
| return IDS_NEARBY_FILE_ATTACHMENTS_NOT_CAPITALIZED_UNKNOWN; |
| case FileAttachment::Type::kVideo: |
| return IDS_NEARBY_FILE_ATTACHMENTS_NOT_CAPITALIZED_VIDEOS; |
| default: |
| return IDS_NEARBY_NOT_CAPITALIZED_UNKNOWN_ATTACHMENTS; |
| } |
| } |
| |
| int GetTextAttachmentsCapitalizedStringId( |
| const std::vector<TextAttachment>& texts) { |
| switch (GetCommonTextAttachmentType(texts)) { |
| case TextAttachment::Type::kAddress: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_CAPITALIZED_ADDRESSES; |
| case TextAttachment::Type::kPhoneNumber: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_CAPITALIZED_PHONE_NUMBERS; |
| case TextAttachment::Type::kText: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_CAPITALIZED_UNKNOWN; |
| case TextAttachment::Type::kUrl: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_CAPITALIZED_LINKS; |
| default: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_CAPITALIZED_UNKNOWN; |
| } |
| } |
| |
| int GetTextAttachmentsNotCapitalizedStringId( |
| const std::vector<TextAttachment>& texts) { |
| switch (GetCommonTextAttachmentType(texts)) { |
| case TextAttachment::Type::kAddress: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_NOT_CAPITALIZED_ADDRESSES; |
| case TextAttachment::Type::kPhoneNumber: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_NOT_CAPITALIZED_PHONE_NUMBERS; |
| case TextAttachment::Type::kText: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_NOT_CAPITALIZED_UNKNOWN; |
| case TextAttachment::Type::kUrl: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_NOT_CAPITALIZED_LINKS; |
| default: |
| return IDS_NEARBY_TEXT_ATTACHMENTS_NOT_CAPITALIZED_UNKNOWN; |
| } |
| } |
| |
| std::u16string GetAttachmentsString(const ShareTarget& share_target, |
| bool use_capitalized_attachments) { |
| size_t file_count = share_target.file_attachments.size(); |
| size_t text_count = share_target.text_attachments.size(); |
| int resource_id = use_capitalized_attachments |
| ? IDS_NEARBY_CAPITALIZED_UNKNOWN_ATTACHMENTS |
| : IDS_NEARBY_NOT_CAPITALIZED_UNKNOWN_ATTACHMENTS; |
| |
| if (file_count > 0 && text_count == 0) |
| resource_id = use_capitalized_attachments |
| ? GetFileAttachmentsCapitalizedStringId( |
| share_target.file_attachments) |
| : GetFileAttachmentsNotCapitalizedStringId( |
| share_target.file_attachments); |
| |
| if (text_count > 0 && file_count == 0) |
| resource_id = use_capitalized_attachments |
| ? GetTextAttachmentsCapitalizedStringId( |
| share_target.text_attachments) |
| : GetTextAttachmentsNotCapitalizedStringId( |
| share_target.text_attachments); |
| |
| return l10n_util::GetPluralStringFUTF16(resource_id, text_count + file_count); |
| } |
| |
| std::u16string FormatNotificationTitle(const ShareTarget& share_target, |
| int resource_id, |
| bool use_capitalized_attachments) { |
| std::u16string attachments = |
| GetAttachmentsString(share_target, use_capitalized_attachments); |
| |
| std::u16string device_name = base::UTF8ToUTF16(share_target.device_name); |
| size_t attachment_count = share_target.file_attachments.size() + |
| share_target.text_attachments.size(); |
| |
| if (!share_target.wifi_credentials_attachments.empty()) { |
| std::u16string network_name = |
| base::UTF8ToUTF16(share_target.wifi_credentials_attachments[0].ssid()); |
| return base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(resource_id), {network_name, device_name}, |
| /*offsets=*/nullptr); |
| } else { |
| return base::ReplaceStringPlaceholders( |
| l10n_util::GetPluralStringFUTF16(resource_id, attachment_count), |
| {attachments, device_name}, /*offsets=*/nullptr); |
| } |
| } |
| |
| std::u16string GetProgressNotificationTitle(const ShareTarget& share_target) { |
| if (!share_target.wifi_credentials_attachments.empty()) { |
| return FormatNotificationTitle( |
| share_target, |
| IDS_NEARBY_NOTIFICATION_RECEIVE_PROGRESS_TITLE_WIFI_CREDENTIALS, |
| /*use_capitalized_attachments=*/false); |
| } else { |
| return FormatNotificationTitle( |
| share_target, |
| share_target.is_incoming |
| ? IDS_NEARBY_NOTIFICATION_RECEIVE_PROGRESS_TITLE |
| : IDS_NEARBY_NOTIFICATION_SEND_PROGRESS_TITLE, |
| /*use_capitalized_attachments=*/false); |
| } |
| } |
| |
| std::u16string GetSuccessNotificationTitle(const ShareTarget& share_target) { |
| if (!share_target.wifi_credentials_attachments.empty()) { |
| return FormatNotificationTitle( |
| share_target, |
| IDS_NEARBY_NOTIFICATION_RECEIVE_SUCCESS_TITLE_WIFI_CREDENTIALS, |
| /*use_capitalized_attachments=*/false); |
| } else { |
| return FormatNotificationTitle( |
| share_target, |
| share_target.is_incoming ? IDS_NEARBY_NOTIFICATION_RECEIVE_SUCCESS_TITLE |
| : IDS_NEARBY_NOTIFICATION_SEND_SUCCESS_TITLE, |
| /*use_capitalized_attachments=*/true); |
| } |
| } |
| |
| std::u16string GetFailureNotificationTitle(const ShareTarget& share_target) { |
| if (!share_target.wifi_credentials_attachments.empty()) { |
| return FormatNotificationTitle( |
| share_target, |
| IDS_NEARBY_NOTIFICATION_RECEIVE_FAILURE_TITLE_WIFI_CREDENTIALS, |
| /*use_capitalized_attachments=*/false); |
| } else { |
| return FormatNotificationTitle( |
| share_target, |
| share_target.is_incoming ? IDS_NEARBY_NOTIFICATION_RECEIVE_FAILURE_TITLE |
| : IDS_NEARBY_NOTIFICATION_SEND_FAILURE_TITLE, |
| /*use_capitalized_attachments=*/false); |
| } |
| } |
| |
| std::optional<std::u16string> GetFailureNotificationMessage( |
| TransferMetadata::Status status) { |
| switch (status) { |
| case TransferMetadata::Status::kTimedOut: |
| return l10n_util::GetStringUTF16(IDS_NEARBY_ERROR_TIME_OUT); |
| case TransferMetadata::Status::kNotEnoughSpace: |
| return l10n_util::GetStringUTF16(IDS_NEARBY_ERROR_NOT_ENOUGH_SPACE); |
| case TransferMetadata::Status::kUnsupportedAttachmentType: |
| return l10n_util::GetStringUTF16(IDS_NEARBY_ERROR_UNSUPPORTED_FILE_TYPE); |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| std::u16string GetConnectionRequestNotificationMessage( |
| const ShareTarget& share_target, |
| const TransferMetadata& transfer_metadata) { |
| std::u16string attachments = |
| GetAttachmentsString(share_target, /*use_capitalized_attachments=*/false); |
| std::u16string device_name = base::UTF8ToUTF16(share_target.device_name); |
| |
| size_t attachment_count = share_target.file_attachments.size() + |
| share_target.text_attachments.size(); |
| std::u16string message; |
| if (!share_target.wifi_credentials_attachments.empty()) { |
| message = base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_CONNECTION_REQUEST_MESSAGE_WIFI_CREDENTIALS), |
| device_name, /*offsets=*/nullptr); |
| } else { |
| message = base::ReplaceStringPlaceholders( |
| l10n_util::GetPluralStringFUTF16( |
| IDS_NEARBY_NOTIFICATION_CONNECTION_REQUEST_MESSAGE, |
| attachment_count), |
| {device_name, attachments}, /*offsets=*/nullptr); |
| } |
| |
| if (transfer_metadata.token()) { |
| std::u16string token = l10n_util::GetStringFUTF16( |
| IDS_NEARBY_SECURE_CONNECTION_ID, |
| base::UTF8ToUTF16(*transfer_metadata.token())); |
| message = base::StrCat({message, u"\n", token}); |
| } |
| |
| return message; |
| } |
| |
| std::optional<std::u16string> GetReceivedNotificationTextMessage( |
| const ShareTarget& share_target) { |
| size_t text_count = share_target.text_attachments.size(); |
| if (text_count < 1) { |
| return std::nullopt; |
| } |
| |
| const TextAttachment& attachment = share_target.text_attachments[0]; |
| return base::UTF8ToUTF16(attachment.GetDescription()); |
| } |
| |
| ui::ImageModel GetImageFromShareTarget(const ShareTarget& share_target) { |
| // TODO(crbug.com/40138752): Create or get profile picture of |share_target|. |
| return ui::ImageModel(); |
| } |
| |
| NearbyNotificationManager::ReceivedContentType GetReceivedContentType( |
| const ShareTarget& share_target) { |
| if (!share_target.wifi_credentials_attachments.empty()) { |
| return NearbyNotificationManager::ReceivedContentType::kWifiCredentials; |
| } |
| |
| if (!share_target.text_attachments.empty()) { |
| const TextAttachment& file = share_target.text_attachments[0]; |
| if (share_target.text_attachments.size() == 1 && |
| file.type() == sharing::mojom::TextMetadata::Type::kUrl) { |
| return NearbyNotificationManager::ReceivedContentType::kSingleUrl; |
| } |
| |
| return NearbyNotificationManager::ReceivedContentType::kText; |
| } |
| |
| if (share_target.file_attachments.size() != 1) |
| return NearbyNotificationManager::ReceivedContentType::kFiles; |
| |
| const FileAttachment& file = share_target.file_attachments[0]; |
| if (file.type() == sharing::mojom::FileMetadata::Type::kImage) |
| return NearbyNotificationManager::ReceivedContentType::kSingleImage; |
| |
| return NearbyNotificationManager::ReceivedContentType::kFiles; |
| } |
| |
| class ProgressNotificationDelegate : public NearbyNotificationDelegate { |
| public: |
| explicit ProgressNotificationDelegate(NearbyNotificationManager* manager, |
| bool awaiting_remote_acceptance) |
| : manager_(manager), |
| awaiting_remote_acceptance_(awaiting_remote_acceptance) {} |
| ~ProgressNotificationDelegate() override = default; |
| |
| // NearbyNotificationDelegate: |
| void OnClick(const std::string& notification_id, |
| const std::optional<int>& action_index) override { |
| // Clicking on the notification is a noop. |
| if (!action_index) |
| return; |
| // Clicking on the only (cancel) button cancels the transfer. |
| DCHECK_EQ(0, *action_index); |
| |
| // In the receiving case, the progress notification is showed after the |
| // transfer is accepted, but before the |TransferMetadata::Status| is |
| // actually |kInProgress|. In this case, it is more appropriate to reject |
| // the transfer, but as far as the user is concerned, it looks like a |
| // cancellation. |
| if (awaiting_remote_acceptance_) |
| manager_->RejectTransfer(); |
| else |
| manager_->CancelTransfer(); |
| } |
| |
| void OnClose(const std::string& notification_id) override { |
| if (awaiting_remote_acceptance_) |
| manager_->RejectTransfer(); |
| else |
| manager_->CancelTransfer(); |
| } |
| |
| private: |
| raw_ptr<NearbyNotificationManager> manager_; |
| bool awaiting_remote_acceptance_ = false; |
| }; |
| |
| class ConnectionRequestNotificationDelegate |
| : public NearbyNotificationDelegate { |
| public: |
| explicit ConnectionRequestNotificationDelegate( |
| NearbyNotificationManager* manager) |
| : manager_(manager) {} |
| ~ConnectionRequestNotificationDelegate() override = default; |
| |
| // NearbyNotificationDelegate: |
| void OnClick(const std::string& notification_id, |
| const std::optional<int>& action_index) override { |
| // Clicking on the notification is a noop. |
| if (!action_index) |
| return; |
| |
| switch (*action_index) { |
| case 0: |
| manager_->AcceptTransfer(); |
| break; |
| case 1: |
| manager_->RejectTransfer(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void OnClose(const std::string& notification_id) override { |
| manager_->RejectTransfer(); |
| } |
| |
| private: |
| raw_ptr<NearbyNotificationManager> manager_; |
| }; |
| |
| class ReceivedImageDecoder : public ImageDecoder::ImageRequest { |
| public: |
| using ImageCallback = base::OnceCallback<void(const SkBitmap& decoded_image)>; |
| |
| explicit ReceivedImageDecoder(ImageCallback callback) |
| : callback_(std::move(callback)) {} |
| ~ReceivedImageDecoder() override = default; |
| |
| void DecodeImage(const std::optional<base::FilePath>& image_path) { |
| if (!image_path) { |
| OnDecodeImageFailed(); |
| return; |
| } |
| |
| auto contents = std::make_unique<std::string>(); |
| auto* contents_ptr = contents.get(); |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&base::ReadFileToString, *image_path, contents_ptr), |
| base::BindOnce(&ReceivedImageDecoder::OnFileRead, |
| weak_ptr_factory_.GetWeakPtr(), std::move(contents))); |
| } |
| |
| // ImageDecoder::ImageRequest implementation: |
| void OnImageDecoded(const SkBitmap& decoded_image) override { |
| std::move(callback_).Run(decoded_image); |
| delete this; |
| } |
| |
| void OnDecodeImageFailed() override { |
| std::move(callback_).Run(SkBitmap()); |
| delete this; |
| } |
| |
| private: |
| void OnFileRead(std::unique_ptr<std::string> contents, |
| bool is_contents_read) { |
| if (!is_contents_read || !contents || contents->empty()) { |
| CD_LOG(VERBOSE, Feature::NS) << __func__ << ": Image contents not found."; |
| OnDecodeImageFailed(); |
| return; |
| } |
| |
| ImageDecoder::Start(this, std::move(*contents)); |
| } |
| |
| ImageCallback callback_; |
| base::WeakPtrFactory<ReceivedImageDecoder> weak_ptr_factory_{this}; |
| }; |
| |
| class SuccessNotificationDelegate : public NearbyNotificationDelegate { |
| public: |
| SuccessNotificationDelegate( |
| NearbyNotificationManager* manager, |
| Profile* profile, |
| ShareTarget share_target, |
| NearbyNotificationManager::ReceivedContentType type, |
| const SkBitmap& image, |
| base::OnceCallback< |
| void(NearbyNotificationManager::SuccessNotificationAction)> |
| testing_callback) |
| : manager_(manager), |
| profile_(profile), |
| share_target_(std::move(share_target)), |
| type_(type), |
| image_(image), |
| testing_callback_(std::move(testing_callback)) {} |
| ~SuccessNotificationDelegate() override = default; |
| |
| // NearbyNotificationDelegate: |
| void OnClick(const std::string& notification_id, |
| const std::optional<int>& action_index) override { |
| switch (type_) { |
| case NearbyNotificationManager::ReceivedContentType::kText: |
| if (action_index.has_value() && action_index.value() == 0) { |
| // Don't overwrite clipboard if user clicks notification body |
| CopyTextToClipboard(); |
| } |
| break; |
| case NearbyNotificationManager::ReceivedContentType::kSingleUrl: |
| if (!action_index.has_value()) { |
| OpenTextLink(); |
| break; |
| } |
| switch (*action_index) { |
| case 0: |
| OpenTextLink(); |
| break; |
| case 1: |
| CopyTextToClipboard(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| break; |
| case NearbyNotificationManager::ReceivedContentType::kSingleImage: |
| if (!action_index.has_value()) { |
| OpenDownloadsFolder(); |
| break; |
| } |
| switch (*action_index) { |
| case 0: |
| OpenDownloadsFolder(); |
| break; |
| case 1: |
| CopyImageToClipboard(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| break; |
| case NearbyNotificationManager::ReceivedContentType::kFiles: |
| OpenDownloadsFolder(); |
| break; |
| case NearbyNotificationManager::ReceivedContentType::kWifiCredentials: |
| OpenWifiNetworksList(); |
| break; |
| } |
| |
| manager_->CloseSuccessNotification(notification_id); |
| } |
| |
| void OnClose(const std::string& notification_id) override { |
| manager_->CloseSuccessNotification(notification_id); |
| } |
| |
| private: |
| void OpenDownloadsFolder() { |
| platform_util::OpenItem( |
| profile_, |
| DownloadPrefs::FromDownloadManager(profile_->GetDownloadManager()) |
| ->DownloadPath(), |
| platform_util::OPEN_FOLDER, platform_util::OpenOperationCallback()); |
| |
| if (testing_callback_) { |
| std::move(testing_callback_) |
| .Run(NearbyNotificationManager::SuccessNotificationAction:: |
| kOpenDownloads); |
| } |
| } |
| |
| void OpenTextLink() { |
| const std::string& url = share_target_.text_attachments[0].text_body(); |
| manager_->OpenURL(GURL(url)); |
| |
| if (testing_callback_) { |
| std::move(testing_callback_) |
| .Run(NearbyNotificationManager::SuccessNotificationAction::kOpenUrl); |
| } |
| } |
| |
| void CopyTextToClipboard() { |
| DCHECK_GT(share_target_.text_attachments.size(), 0u); |
| const std::string& text = share_target_.text_attachments[0].text_body(); |
| ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste) |
| .WriteText(base::UTF8ToUTF16(text)); |
| |
| if (testing_callback_) { |
| std::move(testing_callback_) |
| .Run(NearbyNotificationManager::SuccessNotificationAction::kCopyText); |
| } |
| } |
| |
| void CopyImageToClipboard() { |
| DCHECK(!image_.isNull()); |
| ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste) |
| .WriteImage(image_); |
| |
| if (testing_callback_) { |
| std::move(testing_callback_) |
| .Run( |
| NearbyNotificationManager::SuccessNotificationAction::kCopyImage); |
| } |
| } |
| |
| void OpenWifiNetworksList() { |
| manager_->OpenWifiNetworksList(); |
| |
| if (testing_callback_) { |
| std::move(testing_callback_) |
| .Run(NearbyNotificationManager::SuccessNotificationAction:: |
| kOpenWifiNetworksList); |
| } |
| } |
| |
| raw_ptr<NearbyNotificationManager> manager_; |
| raw_ptr<Profile> profile_; |
| ShareTarget share_target_; |
| NearbyNotificationManager::ReceivedContentType type_; |
| SkBitmap image_; |
| base::OnceCallback<void(NearbyNotificationManager::SuccessNotificationAction)> |
| testing_callback_; |
| }; |
| |
| class NearbyDeviceTryingToShareNotificationDelegate |
| : public NearbyNotificationDelegate { |
| public: |
| explicit NearbyDeviceTryingToShareNotificationDelegate( |
| NearbyNotificationManager* manager) |
| : manager_(manager) {} |
| ~NearbyDeviceTryingToShareNotificationDelegate() override = default; |
| |
| // NearbyNotificationDelegate: |
| void OnClick(const std::string& notification_id, |
| const std::optional<int>& action_index) override { |
| if (!action_index) { |
| return; |
| } |
| |
| switch (*action_index) { |
| case 0: |
| manager_->OnNearbyDeviceTryingToShareClicked(); |
| break; |
| case 1: |
| manager_->OnNearbyDeviceTryingToShareDismissed( |
| /*did_click_dismiss=*/true); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void OnClose(const std::string& notification_id) override { |
| manager_->OnNearbyDeviceTryingToShareDismissed(/*did_click_dismiss=*/false); |
| } |
| |
| private: |
| raw_ptr<NearbyNotificationManager> manager_; |
| }; |
| |
| class NearbyVisibilityReminderNotificationDelegate |
| : public NearbyNotificationDelegate { |
| public: |
| explicit NearbyVisibilityReminderNotificationDelegate( |
| NearbyNotificationManager* manager) |
| : manager_(manager) {} |
| |
| ~NearbyVisibilityReminderNotificationDelegate() override = default; |
| |
| void OnClick(const std::string& notification_id, |
| const std::optional<int>& action_index) override { |
| if (!action_index) { |
| // Open settings when user click the notification. |
| manager_->OnNearbyVisibilityReminderClicked(); |
| return; |
| } |
| |
| switch (*action_index) { |
| case 0: |
| manager_->OnNearbyVisibilityReminderClicked(); |
| break; |
| case 1: |
| manager_->OnNearbyVisibilityReminderDismissed(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void OnClose(const std::string& notification_id) override { |
| manager_->OnNearbyVisibilityReminderDismissed(); |
| } |
| |
| private: |
| raw_ptr<NearbyNotificationManager> manager_; |
| }; |
| |
| bool ShouldShowNearbyDeviceTryingToShareNotification( |
| PrefService* pref_service) { |
| base::Time last_dismissed = pref_service->GetTime( |
| prefs::kNearbySharingNearbyDeviceTryingToShareDismissedTimePrefName); |
| if (last_dismissed.is_null()) |
| return true; |
| |
| base::TimeDelta last_dismissed_delta = base::Time::Now() - last_dismissed; |
| if (last_dismissed_delta < |
| NearbyNotificationManager::kNearbyDeviceTryingToShareDismissedTimeout) { |
| CD_LOG(VERBOSE, Feature::NS) |
| << "Not showing onboarding notification: the user recently " |
| "dismissed the notification."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ShouldShowNearbyVisibilityReminderNotification(PrefService* pref_service) { |
| nearby_share::mojom::Visibility visibility = |
| static_cast<nearby_share::mojom::Visibility>(pref_service->GetInteger( |
| prefs::kNearbySharingBackgroundVisibilityName)); |
| |
| return visibility == nearby_share::mojom::Visibility::kAllContacts || |
| visibility == nearby_share::mojom::Visibility::kSelectedContacts; |
| } |
| |
| void UpdateNearbyDeviceTryingToShareDismissedTime(PrefService* pref_service) { |
| pref_service->SetTime( |
| prefs::kNearbySharingNearbyDeviceTryingToShareDismissedTimePrefName, |
| base::Time::Now()); |
| } |
| |
| bool ShouldClearNotification( |
| std::optional<TransferMetadata::Status> last_status, |
| TransferMetadata::Status new_status) { |
| if (!last_status) |
| return true; |
| |
| // While receiving and waiting for the sender to accept, we are showing a |
| // progress notification with 0% progress. We need not close the |
| // progress notification when we move to showing determinate progress. |
| if (*last_status == TransferMetadata::Status::kAwaitingRemoteAcceptance && |
| new_status == TransferMetadata::Status::kInProgress) |
| return false; |
| |
| // In all other cases, if the status has changed, the previous notification |
| // should be cleared. |
| return *last_status != new_status; |
| } |
| |
| } // namespace |
| |
| // static |
| constexpr base::TimeDelta |
| NearbyNotificationManager::kNearbyDeviceTryingToShareDismissedTimeout; |
| |
| void NearbyNotificationManager::SettingsOpener::ShowSettingsPage( |
| Profile* profile, |
| const std::string& sub_page) { |
| chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile, |
| sub_page); |
| } |
| |
| NearbyNotificationManager::NearbyNotificationManager( |
| NotificationDisplayService* notification_display_service, |
| NearbySharingService* nearby_service, |
| PrefService* pref_service, |
| Profile* profile) |
| : notification_display_service_(notification_display_service), |
| nearby_service_(nearby_service), |
| pref_service_(pref_service), |
| profile_(profile), |
| settings_opener_(std::make_unique<SettingsOpener>()) { |
| DCHECK(notification_display_service_); |
| DCHECK(nearby_service_); |
| DCHECK(pref_service_); |
| nearby_service_->AddObserver(this); |
| nearby_service_->RegisterReceiveSurface( |
| this, NearbySharingService::ReceiveSurfaceState::kBackground); |
| nearby_service_->RegisterSendSurface( |
| this, this, NearbySharingService::SendSurfaceState::kBackground); |
| } |
| |
| NearbyNotificationManager::~NearbyNotificationManager() { |
| nearby_service_->RemoveObserver(this); |
| nearby_service_->UnregisterReceiveSurface(this); |
| nearby_service_->UnregisterSendSurface(this, this); |
| } |
| |
| void NearbyNotificationManager::OnTransferUpdate( |
| const ShareTarget& share_target, |
| const TransferMetadata& transfer_metadata) { |
| if (!share_target_) |
| share_target_ = share_target; |
| DCHECK_EQ(share_target_->id, share_target.id); |
| |
| if (ShouldClearNotification(last_transfer_status_, |
| transfer_metadata.status())) { |
| // Close any previous notifications, to allow subsequent high-priority |
| // notifications to pop up. |
| CloseTransfer(); |
| } |
| last_transfer_status_ = transfer_metadata.status(); |
| |
| switch (transfer_metadata.status()) { |
| case TransferMetadata::Status::kInProgress: |
| ShowProgress(share_target, transfer_metadata); |
| break; |
| case TransferMetadata::Status::kCancelled: |
| // Only show the notification if the remote cancelled. |
| if (!nearby_service_->DidLocalUserCancelTransfer(share_target)) |
| ShowCancelled(share_target); |
| break; |
| case TransferMetadata::Status::kAwaitingRemoteAcceptanceFailed: |
| case TransferMetadata::Status::kExternalProviderLaunched: |
| // Any previous notifications have been closed with the status change |
| // check above that called CloseTransfer(). No notification is currently |
| // shown for these statuses, so break. |
| break; |
| case TransferMetadata::Status::kAwaitingRemoteAcceptance: |
| // Only incoming transfers are handled via notifications. |
| if (share_target.is_incoming) |
| // Show a progress notification with 0% progress while |
| // waiting for the sender to accept. |
| ShowProgress(share_target, transfer_metadata); |
| break; |
| case TransferMetadata::Status::kAwaitingLocalConfirmation: |
| // Only incoming transfers are handled via notifications. |
| // Don't show notification for auto-accept self shares. |
| if (share_target.is_incoming && !share_target.CanAutoAccept()) { |
| ShowConnectionRequest(share_target, transfer_metadata); |
| } |
| break; |
| case TransferMetadata::Status::kComplete: |
| ShowSuccess(share_target); |
| break; |
| case TransferMetadata::Status::kRejected: |
| case TransferMetadata::Status::kTimedOut: |
| case TransferMetadata::Status::kFailed: |
| case TransferMetadata::Status::kNotEnoughSpace: |
| case TransferMetadata::Status::kUnsupportedAttachmentType: |
| case TransferMetadata::Status::kDecodeAdvertisementFailed: |
| case TransferMetadata::Status::kMissingTransferUpdateCallback: |
| case TransferMetadata::Status::kMissingShareTarget: |
| case TransferMetadata::Status::kMissingEndpointId: |
| case TransferMetadata::Status::kMissingPayloads: |
| case TransferMetadata::Status::kPairedKeyVerificationFailed: |
| case TransferMetadata::Status::kInvalidIntroductionFrame: |
| case TransferMetadata::Status::kIncompletePayloads: |
| case TransferMetadata::Status::kFailedToCreateShareTarget: |
| case TransferMetadata::Status::kFailedToInitiateOutgoingConnection: |
| case TransferMetadata::Status::kFailedToReadOutgoingConnectionResponse: |
| case TransferMetadata::Status::kUnexpectedDisconnection: |
| ShowFailure(share_target, transfer_metadata); |
| break; |
| case TransferMetadata::Status::kUnknown: |
| case TransferMetadata::Status::kConnecting: |
| case TransferMetadata::Status::kMediaUnavailable: |
| case TransferMetadata::Status::kMediaDownloading: |
| // Ignore |
| break; |
| } |
| |
| if (transfer_metadata.is_final_status()) { |
| share_target_.reset(); |
| last_transfer_status_.reset(); |
| } |
| } |
| |
| void NearbyNotificationManager::OnShareTargetDiscovered( |
| ShareTarget share_target) { |
| // Nothing to do here. |
| } |
| |
| void NearbyNotificationManager::OnShareTargetLost(ShareTarget share_target) { |
| // Nothing to do here. |
| } |
| |
| void NearbyNotificationManager::OnNearbyProcessStopped() { |
| if (share_target_ && last_transfer_status_) { |
| CloseTransfer(); |
| ShowFailure( |
| *share_target_, |
| TransferMetadataBuilder().set_status(*last_transfer_status_).build()); |
| } |
| share_target_ = std::nullopt; |
| last_transfer_status_ = std::nullopt; |
| } |
| |
| void NearbyNotificationManager::OnFastInitiationDevicesDetected() { |
| ShowNearbyDeviceTryingToShare(); |
| } |
| |
| void NearbyNotificationManager::OnFastInitiationDevicesNotDetected() { |
| CloseNearbyDeviceTryingToShare(); |
| } |
| |
| void NearbyNotificationManager::OnFastInitiationScanningStopped() { |
| CloseNearbyDeviceTryingToShare(); |
| } |
| |
| void NearbyNotificationManager::ShowProgress( |
| const ShareTarget& share_target, |
| const TransferMetadata& transfer_metadata) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| message_center::Notification notification = |
| CreateNearbyNotification(kNearbyInProgressNotificationId); |
| notification.set_type(message_center::NOTIFICATION_TYPE_PROGRESS); |
| notification.set_title(GetProgressNotificationTitle(share_target)); |
| notification.set_never_timeout(true); |
| notification.set_pinned(true); |
| notification.set_priority(message_center::NotificationPriority::MAX_PRIORITY); |
| |
| // Show 0% progress while waiting for remote device to accept. |
| if (transfer_metadata.status() == TransferMetadata::Status::kInProgress) |
| notification.set_progress(transfer_metadata.progress()); |
| else |
| notification.set_progress(0); |
| |
| std::vector<message_center::ButtonInfo> notification_actions; |
| notification_actions.emplace_back(l10n_util::GetStringUTF16(IDS_APP_CANCEL)); |
| notification.set_buttons(notification_actions); |
| |
| delegate_map_[notification.id()] = |
| std::make_unique<ProgressNotificationDelegate>( |
| this, /*awaiting_remote_acceptance=*/transfer_metadata.status() == |
| TransferMetadata::Status::kAwaitingRemoteAcceptance); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void NearbyNotificationManager::ShowConnectionRequest( |
| const ShareTarget& share_target, |
| const TransferMetadata& transfer_metadata) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| message_center::Notification notification = |
| CreateNearbyNotification(kNearbyInProgressNotificationId); |
| notification.set_title( |
| features::IsNameEnabled() |
| ? NearbyShareResourceGetter::GetInstance()->GetStringWithFeatureName( |
| IDS_NEARBY_NOTIFICATION_CONNECTION_REQUEST_TITLE_PH) |
| : l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_CONNECTION_REQUEST_TITLE)); |
| notification.set_message( |
| GetConnectionRequestNotificationMessage(share_target, transfer_metadata)); |
| notification.set_icon(GetImageFromShareTarget(share_target)); |
| notification.set_never_timeout(true); |
| notification.set_priority(message_center::NotificationPriority::MAX_PRIORITY); |
| |
| std::vector<message_center::ButtonInfo> notification_actions; |
| notification_actions.emplace_back( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_ACCEPT_ACTION)); |
| notification_actions.emplace_back( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_DECLINE_ACTION)); |
| notification.set_buttons(notification_actions); |
| |
| delegate_map_[notification.id()] = |
| std::make_unique<ConnectionRequestNotificationDelegate>(this); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void NearbyNotificationManager::ShowNearbyDeviceTryingToShare() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!ShouldShowNearbyDeviceTryingToShareNotification(pref_service_)) |
| return; |
| |
| CD_LOG(INFO, Feature::NS) << "Showing fast initiation notification."; |
| |
| message_center::Notification notification = |
| CreateNearbyNotification(kNearbyDeviceTryingToShareNotificationId); |
| |
| bool is_onboarding_complete = pref_service_->GetBoolean( |
| prefs::kNearbySharingOnboardingCompletePrefName); |
| |
| notification.set_title( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_ONBOARDING_TITLE)); |
| |
| const std::u16string onboarding_message = |
| features::IsNameEnabled() |
| ? NearbyShareResourceGetter::GetInstance()->GetStringWithFeatureName( |
| IDS_NEARBY_NOTIFICATION_ONBOARDING_MESSAGE_PH) |
| : l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ONBOARDING_MESSAGE); |
| |
| notification.set_message(is_onboarding_complete |
| ? l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_GO_VISIBLE_MESSAGE) |
| : onboarding_message); |
| |
| std::vector<message_center::ButtonInfo> notification_actions; |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| is_onboarding_complete ? IDS_NEARBY_NOTIFICATION_GO_VISIBLE_ACTION |
| : IDS_NEARBY_NOTIFICATION_SET_UP_ACTION)); |
| notification_actions.emplace_back( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_DISMISS_ACTION)); |
| notification.set_buttons(notification_actions); |
| |
| delegate_map_[kNearbyDeviceTryingToShareNotificationId] = |
| std::make_unique<NearbyDeviceTryingToShareNotificationDelegate>(this); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| |
| last_device_nearby_sharing_notification_shown_timestamp_ = |
| base::TimeTicks::Now(); |
| |
| if (is_onboarding_complete) { |
| RecordNearbyShareDeviceNearbySharingNotificationFlowEvent( |
| NearbyShareBackgroundScanningDeviceNearbySharingNotificationFlowEvent:: |
| kNotificationShown); |
| } else { |
| RecordNearbyShareSetupNotificationFlowEvent( |
| NearbyShareBackgroundScanningSetupNotificationFlowEvent:: |
| kNotificationShown); |
| } |
| } |
| |
| void NearbyNotificationManager::ShowSuccess(const ShareTarget& share_target) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!share_target.is_incoming) { |
| std::string notification_id = |
| CreateNotificationIdForShareTarget(share_target); |
| message_center::Notification notification = |
| CreateNearbyNotification(notification_id); |
| notification.set_title(GetSuccessNotificationTitle(share_target)); |
| |
| delegate_map_.erase(notification_id); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| return; |
| } |
| |
| ReceivedContentType type = GetReceivedContentType(share_target); |
| |
| if (type != ReceivedContentType::kSingleImage) { |
| ShowIncomingSuccess(share_target, type, /*image=*/SkBitmap()); |
| return; |
| } |
| |
| // ReceivedContentType::kSingleImage means exactly one image file. |
| DCHECK_EQ(1u, share_target.file_attachments.size()); |
| |
| // ReceivedImageDecoder will delete itself. |
| auto* image_decoder = new ReceivedImageDecoder( |
| base::BindOnce(&NearbyNotificationManager::ShowIncomingSuccess, |
| weak_ptr_factory_.GetWeakPtr(), share_target, type)); |
| image_decoder->DecodeImage(share_target.file_attachments[0].file_path()); |
| } |
| |
| void NearbyNotificationManager::ShowIncomingSuccess( |
| const ShareTarget& share_target, |
| ReceivedContentType type, |
| const SkBitmap& image) { |
| std::string notification_id = |
| CreateNotificationIdForShareTarget(share_target); |
| message_center::Notification notification = |
| CreateNearbyNotification(notification_id); |
| notification.set_title(GetSuccessNotificationTitle(share_target)); |
| |
| // Revert to generic file handling if image decoding failed. |
| if (type == ReceivedContentType::kSingleImage && image.isNull()) |
| type = ReceivedContentType::kFiles; |
| |
| if (!image.isNull()) { |
| notification.set_type(message_center::NOTIFICATION_TYPE_IMAGE); |
| notification.SetImage(gfx::Image::CreateFrom1xBitmap(image)); |
| } |
| |
| std::vector<message_center::ButtonInfo> notification_actions; |
| switch (type) { |
| case ReceivedContentType::kText: { |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ACTION_COPY_TO_CLIPBOARD)); |
| std::optional<std::u16string> message = |
| GetReceivedNotificationTextMessage(share_target); |
| if (message) { |
| notification.set_message(message.value()); |
| } |
| break; |
| } |
| case ReceivedContentType::kSingleUrl: |
| notification_actions.emplace_back( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_ACTION_OPEN_URL)); |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ACTION_COPY_TO_CLIPBOARD)); |
| break; |
| case ReceivedContentType::kSingleImage: |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ACTION_OPEN_FOLDER)); |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ACTION_COPY_TO_CLIPBOARD)); |
| break; |
| case ReceivedContentType::kFiles: |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ACTION_OPEN_FOLDER)); |
| break; |
| case ReceivedContentType::kWifiCredentials: |
| notification_actions.emplace_back(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_ACTION_OPEN_NETWORK_LIST)); |
| break; |
| } |
| notification.set_buttons(notification_actions); |
| |
| delegate_map_[notification_id] = |
| std::make_unique<SuccessNotificationDelegate>( |
| this, profile_, share_target, type, image, |
| std::move(success_action_test_callback_)); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| |
| ash::HoldingSpaceKeyedService* holding_space_keyed_service = |
| ash::HoldingSpaceKeyedServiceFactory::GetInstance()->GetService(profile_); |
| if (holding_space_keyed_service) { |
| for (const auto& file : share_target.file_attachments) { |
| if (file.file_path().has_value()) |
| holding_space_keyed_service->AddItemOfType( |
| ash::HoldingSpaceItem::Type::kNearbyShare, |
| file.file_path().value()); |
| } |
| } |
| } |
| |
| void NearbyNotificationManager::ShowFailure( |
| const ShareTarget& share_target, |
| const TransferMetadata& transfer_metadata) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| std::string notification_id = |
| CreateNotificationIdForShareTarget(share_target); |
| message_center::Notification notification = |
| CreateNearbyNotification(notification_id); |
| notification.set_title(GetFailureNotificationTitle(share_target)); |
| |
| std::optional<std::u16string> message = |
| GetFailureNotificationMessage(transfer_metadata.status()); |
| if (message) { |
| notification.set_message(*message); |
| } |
| |
| delegate_map_.erase(notification_id); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void NearbyNotificationManager::ShowCancelled(const ShareTarget& share_target) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| std::string notification_id = |
| CreateNotificationIdForShareTarget(share_target); |
| message_center::Notification notification = |
| CreateNearbyNotification(notification_id); |
| |
| notification.set_title(base::ReplaceStringPlaceholders( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_SENDER_CANCELLED), |
| base::UTF8ToUTF16(share_target.device_name), nullptr)); |
| |
| delegate_map_.erase(notification_id); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void NearbyNotificationManager::ShowVisibilityReminder() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!ShouldShowNearbyVisibilityReminderNotification(pref_service_)) { |
| return; |
| } |
| |
| message_center::Notification notification = |
| CreateNearbyNotification(kNearbyVisibilityReminderNotificationId); |
| notification.set_title(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_VISIBILITY_REMINDER_TITLE)); |
| notification.set_message(l10n_util::GetStringUTF16( |
| IDS_NEARBY_NOTIFICATION_VISIBILITY_REMINDER_MESSAGE)); |
| |
| std::vector<message_center::ButtonInfo> notification_actions; |
| notification_actions.emplace_back( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_GO_TO_SETTINGS_ACTION)); |
| notification_actions.emplace_back( |
| l10n_util::GetStringUTF16(IDS_NEARBY_NOTIFICATION_DISMISS_ACTION)); |
| |
| notification.set_buttons(notification_actions); |
| |
| delegate_map_[kNearbyVisibilityReminderNotificationId] = |
| std::make_unique<NearbyVisibilityReminderNotificationDelegate>(this); |
| |
| notification_display_service_->Display( |
| NotificationHandler::Type::NEARBY_SHARE, notification, |
| /*metadata=*/nullptr); |
| } |
| |
| void NearbyNotificationManager::CloseTransfer() { |
| delegate_map_.erase(kNearbyInProgressNotificationId); |
| notification_display_service_->Close(NotificationHandler::Type::NEARBY_SHARE, |
| kNearbyInProgressNotificationId); |
| } |
| |
| void NearbyNotificationManager::CloseNearbyDeviceTryingToShare() { |
| delegate_map_.erase(kNearbyDeviceTryingToShareNotificationId); |
| notification_display_service_->Close( |
| NotificationHandler::Type::NEARBY_SHARE, |
| kNearbyDeviceTryingToShareNotificationId); |
| } |
| |
| void NearbyNotificationManager::CloseVisibilityReminder() { |
| delegate_map_.erase(kNearbyVisibilityReminderNotificationId); |
| notification_display_service_->Close(NotificationHandler::Type::NEARBY_SHARE, |
| kNearbyVisibilityReminderNotificationId); |
| } |
| |
| NearbyNotificationDelegate* NearbyNotificationManager::GetNotificationDelegate( |
| const std::string& notification_id) { |
| auto iter = delegate_map_.find(notification_id); |
| if (iter == delegate_map_.end()) |
| return nullptr; |
| |
| return iter->second.get(); |
| } |
| |
| void NearbyNotificationManager::OpenURL(GURL url) { |
| nearby_service_->OpenURL(url); |
| } |
| |
| void NearbyNotificationManager::OpenWifiNetworksList() { |
| settings_opener_->ShowSettingsPage( |
| profile_, chromeos::settings::mojom::kKnownNetworksSubpagePath); |
| } |
| |
| void NearbyNotificationManager::CancelTransfer() { |
| CloseTransfer(); |
| nearby_service_->Cancel(*share_target_, base::DoNothing()); |
| } |
| |
| void NearbyNotificationManager::RejectTransfer() { |
| CloseTransfer(); |
| nearby_service_->Reject(*share_target_, base::DoNothing()); |
| } |
| |
| void NearbyNotificationManager::AcceptTransfer() { |
| // Do not close the notification as it will be replaced soon. |
| nearby_service_->Accept(*share_target_, base::DoNothing()); |
| } |
| |
| void NearbyNotificationManager::OnNearbyDeviceTryingToShareClicked() { |
| CloseNearbyDeviceTryingToShare(); |
| |
| std::string path = |
| std::string(chromeos::settings::mojom::kNearbyShareSubpagePath) + |
| "?receive&entrypoint=notification&"; |
| |
| // Append a timestamp to ensure that the URL that we use to open the receive |
| // dialog on the Nearby Share subpage is unique each time the user clicks on |
| // an instance of this notification. This ensures that the page will reload |
| // and we will restart high visibility mode. Since we don't actually read this |
| // timestamp, it will not cause a problem if the user manually reloads the |
| // page with a stale timestamp. |
| path += "&time=" + GetTimestampString(); |
| settings_opener_->ShowSettingsPage(profile_, path); |
| |
| bool is_onboarding_complete = pref_service_->GetBoolean( |
| prefs::kNearbySharingOnboardingCompletePrefName); |
| if (is_onboarding_complete) { |
| RecordNearbyShareDeviceNearbySharingNotificationFlowEvent( |
| NearbyShareBackgroundScanningDeviceNearbySharingNotificationFlowEvent:: |
| kEnable); |
| RecordNearbyShareDeviceNearbySharingNotificationTimeToAction( |
| base::TimeTicks::Now() - |
| last_device_nearby_sharing_notification_shown_timestamp_); |
| } else { |
| RecordNearbyShareSetupNotificationFlowEvent( |
| NearbyShareBackgroundScanningSetupNotificationFlowEvent::kSetup); |
| RecordNearbyShareSetupNotificationTimeToAction( |
| base::TimeTicks::Now() - |
| last_device_nearby_sharing_notification_shown_timestamp_); |
| } |
| } |
| |
| void NearbyNotificationManager::OnNearbyDeviceTryingToShareDismissed( |
| bool did_click_dismiss) { |
| CloseNearbyDeviceTryingToShare(); |
| UpdateNearbyDeviceTryingToShareDismissedTime(pref_service_); |
| |
| bool is_onboarding_complete = pref_service_->GetBoolean( |
| prefs::kNearbySharingOnboardingCompletePrefName); |
| |
| if (is_onboarding_complete) { |
| RecordNearbyShareDeviceNearbySharingNotificationFlowEvent( |
| did_click_dismiss |
| ? NearbyShareBackgroundScanningDeviceNearbySharingNotificationFlowEvent:: |
| kDismiss |
| : NearbyShareBackgroundScanningDeviceNearbySharingNotificationFlowEvent:: |
| kExit); |
| } else { |
| RecordNearbyShareSetupNotificationFlowEvent( |
| did_click_dismiss |
| ? NearbyShareBackgroundScanningSetupNotificationFlowEvent::kDismiss |
| : NearbyShareBackgroundScanningSetupNotificationFlowEvent::kExit); |
| RecordNearbyShareSetupNotificationTimeToAction( |
| base::TimeTicks::Now() - |
| last_device_nearby_sharing_notification_shown_timestamp_); |
| } |
| } |
| |
| void NearbyNotificationManager::CloseSuccessNotification( |
| const std::string& notification_id) { |
| delegate_map_.erase(notification_id); |
| notification_display_service_->Close(NotificationHandler::Type::NEARBY_SHARE, |
| notification_id); |
| } |
| |
| void NearbyNotificationManager::OnNearbyVisibilityReminderClicked() { |
| CloseVisibilityReminder(); |
| |
| std::string path = |
| std::string(chromeos::settings::mojom::kNearbyShareSubpagePath) + |
| "?visibility"; |
| |
| settings_opener_->ShowSettingsPage(profile_, path); |
| } |
| |
| void NearbyNotificationManager::OnNearbyVisibilityReminderDismissed() { |
| CloseVisibilityReminder(); |
| } |
| |
| void NearbyNotificationManager::SetOnSuccessClickedForTesting( |
| base::OnceCallback<void(SuccessNotificationAction)> callback) { |
| success_action_test_callback_ = std::move(callback); |
| } |
| |
| void NearbyNotificationManager::SetSettingsOpenerForTesting( |
| std::unique_ptr<SettingsOpener> settings_opener) { |
| settings_opener_ = std::move(settings_opener); |
| } |