| // Copyright 2017 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/notifications/notification_platform_bridge_linux.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <set> |
| #include <unordered_map> |
| #include <unordered_set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/barrier_closure.h" |
| #include "base/files/file_util.h" |
| #include "base/i18n/number_formatting.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/nullable_string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task_scheduler/post_task.h" |
| #include "base/version.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/notifications/native_notification_display_service.h" |
| #include "chrome/browser/notifications/notification.h" |
| #include "chrome/browser/notifications/notification_display_service_factory.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/shell_integration_linux.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/notification_service.h" |
| #include "dbus/bus.h" |
| #include "dbus/message.h" |
| #include "dbus/object_proxy.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/image/image_skia.h" |
| |
| namespace { |
| |
| // DBus name / path. |
| const char kFreedesktopNotificationsName[] = "org.freedesktop.Notifications"; |
| const char kFreedesktopNotificationsPath[] = "/org/freedesktop/Notifications"; |
| |
| // DBus methods. |
| const char kMethodCloseNotification[] = "CloseNotification"; |
| const char kMethodGetCapabilities[] = "GetCapabilities"; |
| const char kMethodNotify[] = "Notify"; |
| const char kMethodGetServerInformation[] = "GetServerInformation"; |
| |
| // DBus signals. |
| const char kSignalActionInvoked[] = "ActionInvoked"; |
| const char kSignalNotificationClosed[] = "NotificationClosed"; |
| |
| // Capabilities. |
| const char kCapabilityActionIcons[] = "action-icons"; |
| const char kCapabilityActions[] = "actions"; |
| const char kCapabilityBody[] = "body"; |
| const char kCapabilityBodyHyperlinks[] = "body-hyperlinks"; |
| const char kCapabilityBodyImages[] = "body-images"; |
| const char kCapabilityBodyMarkup[] = "body-markup"; |
| const char kCapabilityIconMulti[] = "icon-multi"; |
| const char kCapabilityIconStatic[] = "icon-static"; |
| const char kCapabilityPersistence[] = "persistence"; |
| const char kCapabilitySound[] = "sound"; |
| |
| // Button IDs. |
| const char kDefaultButtonId[] = "default"; |
| const char kSettingsButtonId[] = "settings"; |
| |
| // The values in this enumeration correspond to those of the |
| // Linux.NotificationPlatformBridge.InitializationStatus histogram, so |
| // the ordering should not be changed. New error codes should be |
| // added at the end, before NUM_ITEMS. |
| enum class ConnectionInitializationStatusCode { |
| SUCCESS = 0, |
| NATIVE_NOTIFICATIONS_NOT_SUPPORTED = 1, |
| MISSING_REQUIRED_CAPABILITIES = 2, |
| COULD_NOT_CONNECT_TO_SIGNALS = 3, |
| INCOMPATIBLE_SPEC_VERSION = 4, |
| NUM_ITEMS |
| }; |
| |
| base::string16 CreateNotificationTitle(const Notification& notification) { |
| base::string16 title; |
| if (notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS) { |
| title += base::FormatPercent(notification.progress()); |
| title += base::UTF8ToUTF16(" - "); |
| } |
| title += notification.title(); |
| return title; |
| } |
| |
| gfx::Image DeepCopyImage(const gfx::Image& image) { |
| if (image.IsEmpty()) |
| return gfx::Image(); |
| std::unique_ptr<gfx::ImageSkia> image_skia(image.CopyImageSkia()); |
| return gfx::Image(*image_skia); |
| } |
| |
| int NotificationPriorityToFdoUrgency(int priority) { |
| enum FdoUrgency { |
| LOW = 0, |
| NORMAL = 1, |
| CRITICAL = 2, |
| }; |
| switch (priority) { |
| case message_center::MIN_PRIORITY: |
| case message_center::LOW_PRIORITY: |
| return LOW; |
| case message_center::HIGH_PRIORITY: |
| case message_center::MAX_PRIORITY: |
| return CRITICAL; |
| default: |
| NOTREACHED(); |
| case message_center::DEFAULT_PRIORITY: |
| return NORMAL; |
| } |
| } |
| |
| // Runs once the profile has been loaded in order to perform a given |
| // |operation| on a notification. |
| void ProfileLoadedCallback(NotificationCommon::Operation operation, |
| NotificationCommon::Type notification_type, |
| const std::string& origin, |
| const std::string& notification_id, |
| int action_index, |
| const base::NullableString16& reply, |
| Profile* profile) { |
| if (!profile) |
| return; |
| |
| auto* display_service = static_cast<NativeNotificationDisplayService*>( |
| NotificationDisplayServiceFactory::GetForProfile(profile)); |
| display_service->ProcessNotificationOperation(operation, notification_type, |
| origin, notification_id, |
| action_index, reply); |
| } |
| |
| void ForwardNotificationOperationOnUiThread( |
| NotificationCommon::Operation operation, |
| NotificationCommon::Type notification_type, |
| const std::string& origin, |
| const std::string& notification_id, |
| int action_index, |
| const std::string& profile_id, |
| bool is_incognito) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| g_browser_process->profile_manager()->LoadProfile( |
| profile_id, is_incognito, |
| base::Bind(&ProfileLoadedCallback, operation, notification_type, origin, |
| notification_id, action_index, base::NullableString16())); |
| } |
| |
| class ResourceFile { |
| public: |
| explicit ResourceFile(const base::FilePath& file_path) |
| : file_path_(file_path) { |
| DCHECK(!file_path.empty()); |
| } |
| ~ResourceFile() { base::DeleteFile(file_path_, false); } |
| |
| const base::FilePath& file_path() const { return file_path_; } |
| |
| private: |
| const base::FilePath file_path_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ResourceFile); |
| }; |
| |
| // Writes |data| to a new temporary file and returns the ResourceFile |
| // that holds it. |
| std::unique_ptr<ResourceFile> WriteDataToTmpFile( |
| const scoped_refptr<base::RefCountedMemory>& data) { |
| int data_len = data->size(); |
| if (data_len == 0) |
| return nullptr; |
| base::FilePath file_path; |
| if (!base::CreateTemporaryFile(&file_path)) |
| return nullptr; |
| |
| auto resource_file = base::MakeUnique<ResourceFile>(file_path); |
| if (base::WriteFile(file_path, data->front_as<char>(), data_len) != |
| data_len) { |
| resource_file.reset(); |
| } |
| return resource_file; |
| } |
| |
| } // namespace |
| |
| // static |
| NotificationPlatformBridge* NotificationPlatformBridge::Create() { |
| return new NotificationPlatformBridgeLinux(); |
| } |
| |
| class NotificationPlatformBridgeLinuxImpl |
| : public NotificationPlatformBridge, |
| public content::NotificationObserver, |
| public base::RefCountedThreadSafe<NotificationPlatformBridgeLinuxImpl> { |
| public: |
| explicit NotificationPlatformBridgeLinuxImpl(scoped_refptr<dbus::Bus> bus) |
| : bus_(bus) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| task_runner_ = base::CreateSingleThreadTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING}); |
| registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING, |
| content::NotificationService::AllSources()); |
| } |
| |
| // InitOnTaskRunner() cannot be posted from within the constructor |
| // because of a race condition. The reference count for |this| |
| // starts out as 0. Posting the Init task would increment the count |
| // to 1. If the task finishes before the constructor returns, the |
| // count will go to 0 and the object would be prematurely |
| // destructed. |
| void Init() { |
| PostTaskToTaskRunnerThread(base::BindOnce( |
| &NotificationPlatformBridgeLinuxImpl::InitOnTaskRunner, this)); |
| } |
| |
| void Display(NotificationCommon::Type notification_type, |
| const std::string& notification_id, |
| const std::string& profile_id, |
| bool is_incognito, |
| const Notification& notification) override { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| // Notifications contain gfx::Image's which have reference counts |
| // that are not thread safe. Because of this, we duplicate the |
| // notification and its images. Wrap the notification in a |
| // unique_ptr to transfer ownership of the notification (and the |
| // non-thread-safe reference counts) to the task runner thread. |
| auto notification_copy = base::MakeUnique<Notification>(notification); |
| notification_copy->set_icon(DeepCopyImage(notification_copy->icon())); |
| notification_copy->set_image(gfx::Image()); |
| notification_copy->set_small_image(gfx::Image()); |
| for (size_t i = 0; i < notification_copy->buttons().size(); i++) |
| notification_copy->SetButtonIcon(i, gfx::Image()); |
| |
| PostTaskToTaskRunnerThread(base::BindOnce( |
| &NotificationPlatformBridgeLinuxImpl::DisplayOnTaskRunner, this, |
| notification_type, notification_id, profile_id, is_incognito, |
| base::Passed(¬ification_copy))); |
| } |
| |
| void Close(const std::string& profile_id, |
| const std::string& notification_id) override { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PostTaskToTaskRunnerThread( |
| base::BindOnce(&NotificationPlatformBridgeLinuxImpl::CloseOnTaskRunner, |
| this, profile_id, notification_id)); |
| } |
| |
| void GetDisplayed( |
| const std::string& profile_id, |
| bool incognito, |
| const GetDisplayedNotificationsCallback& callback) const override { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PostTaskToTaskRunnerThread(base::BindOnce( |
| &NotificationPlatformBridgeLinuxImpl::GetDisplayedOnTaskRunner, this, |
| profile_id, incognito, callback)); |
| } |
| |
| void SetReadyCallback(NotificationBridgeReadyCallback callback) override { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (connected_.has_value()) { |
| std::move(callback).Run(connected_.value()); |
| } else { |
| on_connected_callbacks_.push_back(std::move(callback)); |
| } |
| } |
| |
| void CleanUp() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| PostTaskToTaskRunnerThread(base::BindOnce( |
| &NotificationPlatformBridgeLinuxImpl::CleanUpOnTaskRunner, this)); |
| } |
| |
| private: |
| friend class base::RefCountedThreadSafe<NotificationPlatformBridgeLinuxImpl>; |
| |
| struct NotificationData { |
| NotificationData(NotificationCommon::Type notification_type, |
| const std::string& notification_id, |
| const std::string& profile_id, |
| bool is_incognito, |
| const GURL& origin_url) |
| : notification_type(notification_type), |
| notification_id(notification_id), |
| profile_id(profile_id), |
| is_incognito(is_incognito), |
| origin_url(origin_url) {} |
| |
| // The ID used by the notification server. Will be 0 until the |
| // first "Notify" message completes. |
| uint32_t dbus_id = 0; |
| |
| // Same parameters used by NotificationPlatformBridge::Display(). |
| NotificationCommon::Type notification_type; |
| const std::string notification_id; |
| const std::string profile_id; |
| const bool is_incognito; |
| |
| // A copy of the origin_url from the underlying |
| // message_center::Notification. Used to pass back to |
| // NativeNotificationDisplayService. |
| const GURL origin_url; |
| |
| // Used to keep track of the IDs of the buttons currently displayed |
| // on this notification. The valid range of action IDs is |
| // [action_start, action_end). |
| size_t action_start = 0; |
| size_t action_end = 0; |
| |
| // Temporary resource files associated with the notification that |
| // should be cleaned up when the notification is closed or on |
| // shutdown. |
| std::vector<std::unique_ptr<ResourceFile>> resource_files; |
| }; |
| |
| ~NotificationPlatformBridgeLinuxImpl() override { |
| DCHECK(!bus_); |
| DCHECK(!notification_proxy_); |
| DCHECK(notifications_.empty()); |
| } |
| |
| void Observe(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) override { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type); |
| // The browser process is about to exit. Post the CleanUp() task |
| // while we still can. |
| CleanUp(); |
| } |
| |
| void PostTaskToUiThread(base::OnceClosure closure) const { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| bool success = content::BrowserThread::PostTask( |
| content::BrowserThread::UI, FROM_HERE, std::move(closure)); |
| DCHECK(success); |
| } |
| |
| void PostTaskToTaskRunnerThread(base::OnceClosure closure) const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(task_runner_); |
| bool success = task_runner_->PostTask(FROM_HERE, std::move(closure)); |
| DCHECK(success); |
| } |
| |
| // Sets up the D-Bus connection. |
| void InitOnTaskRunner() { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| // |bus_| may be non-null in unit testing where a fake bus is used. |
| if (!bus_) { |
| dbus::Bus::Options bus_options; |
| bus_options.bus_type = dbus::Bus::SESSION; |
| bus_options.connection_type = dbus::Bus::PRIVATE; |
| bus_options.dbus_task_runner = task_runner_; |
| bus_ = make_scoped_refptr(new dbus::Bus(bus_options)); |
| } |
| |
| notification_proxy_ = |
| bus_->GetObjectProxy(kFreedesktopNotificationsName, |
| dbus::ObjectPath(kFreedesktopNotificationsPath)); |
| if (!notification_proxy_) { |
| OnConnectionInitializationFinishedOnTaskRunner( |
| ConnectionInitializationStatusCode:: |
| NATIVE_NOTIFICATIONS_NOT_SUPPORTED); |
| return; |
| } |
| |
| dbus::MethodCall get_capabilities_call(kFreedesktopNotificationsName, |
| kMethodGetCapabilities); |
| std::unique_ptr<dbus::Response> capabilities_response = |
| notification_proxy_->CallMethodAndBlock( |
| &get_capabilities_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); |
| if (capabilities_response) { |
| dbus::MessageReader reader(capabilities_response.get()); |
| std::vector<std::string> capabilities; |
| reader.PopArrayOfStrings(&capabilities); |
| for (const std::string& capability : capabilities) |
| capabilities_.insert(capability); |
| } |
| RecordMetricsForCapabilities(); |
| |
| dbus::MethodCall get_server_information_call(kFreedesktopNotificationsName, |
| kMethodGetServerInformation); |
| std::unique_ptr<dbus::Response> server_information_response = |
| notification_proxy_->CallMethodAndBlock( |
| &get_server_information_call, |
| dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); |
| if (server_information_response) { |
| dbus::MessageReader reader(server_information_response.get()); |
| std::string spec_version; |
| reader.PopString(&spec_version); // name |
| reader.PopString(&spec_version); // vendor |
| reader.PopString(&spec_version); // version |
| reader.PopString(&spec_version); // spec_version |
| spec_version_ = base::Version(spec_version); |
| } |
| // The minimum supported spec version is 1.1, because this was the |
| // version that added image hints. |
| if (!spec_version_.IsValid() || |
| spec_version_ < base::Version(std::vector<uint32_t>{1, 1})) { |
| OnConnectionInitializationFinishedOnTaskRunner( |
| ConnectionInitializationStatusCode::INCOMPATIBLE_SPEC_VERSION); |
| return; |
| } |
| |
| connected_signals_barrier_ = base::BarrierClosure( |
| 2, base::Bind(&NotificationPlatformBridgeLinuxImpl:: |
| OnConnectionInitializationFinishedOnTaskRunner, |
| this, ConnectionInitializationStatusCode::SUCCESS)); |
| notification_proxy_->ConnectToSignal( |
| kFreedesktopNotificationsName, kSignalActionInvoked, |
| base::Bind(&NotificationPlatformBridgeLinuxImpl::OnActionInvoked, this), |
| base::Bind(&NotificationPlatformBridgeLinuxImpl::OnSignalConnected, |
| this)); |
| notification_proxy_->ConnectToSignal( |
| kFreedesktopNotificationsName, kSignalNotificationClosed, |
| base::Bind(&NotificationPlatformBridgeLinuxImpl::OnNotificationClosed, |
| this), |
| base::Bind(&NotificationPlatformBridgeLinuxImpl::OnSignalConnected, |
| this)); |
| } |
| |
| void CleanUpOnTaskRunner() { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| if (bus_) |
| bus_->ShutdownAndBlock(); |
| bus_ = nullptr; |
| notification_proxy_ = nullptr; |
| notifications_.clear(); |
| } |
| |
| // Makes the "Notify" call to D-Bus. |
| void DisplayOnTaskRunner(NotificationCommon::Type notification_type, |
| const std::string& notification_id, |
| const std::string& profile_id, |
| bool is_incognito, |
| std::unique_ptr<Notification> notification) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| NotificationData* data = |
| FindNotificationData(notification_id, profile_id, is_incognito); |
| if (data) { |
| // Update an existing notification. |
| data->notification_type = notification_type; |
| data->resource_files.clear(); |
| } else { |
| // Send the notification for the first time. |
| data = |
| new NotificationData(notification_type, notification_id, profile_id, |
| is_incognito, notification->origin_url()); |
| notifications_.emplace(data, base::WrapUnique(data)); |
| } |
| |
| dbus::MethodCall method_call(kFreedesktopNotificationsName, kMethodNotify); |
| dbus::MessageWriter writer(&method_call); |
| |
| // app_name passed implicitly via desktop-entry. |
| writer.AppendString(""); |
| |
| writer.AppendUint32(data->dbus_id); |
| |
| // app_icon passed implicitly via desktop-entry. |
| writer.AppendString(""); |
| |
| writer.AppendString( |
| base::UTF16ToUTF8(CreateNotificationTitle(*notification))); |
| |
| std::string body; |
| if (base::ContainsKey(capabilities_, kCapabilityBody)) { |
| body = base::UTF16ToUTF8(notification->message()); |
| const bool body_markup = |
| base::ContainsKey(capabilities_, kCapabilityBodyMarkup); |
| if (body_markup) { |
| base::ReplaceSubstringsAfterOffset(&body, 0, "&", "&"); |
| base::ReplaceSubstringsAfterOffset(&body, 0, "<", "<"); |
| base::ReplaceSubstringsAfterOffset(&body, 0, ">", ">"); |
| } |
| if (notification->type() == message_center::NOTIFICATION_TYPE_MULTIPLE) { |
| for (const auto& item : notification->items()) { |
| if (!body.empty()) |
| body += "\n"; |
| const std::string title = base::UTF16ToUTF8(item.title); |
| const std::string message = base::UTF16ToUTF8(item.message); |
| // TODO(peter): Figure out the right way to internationalize |
| // this for RTL languages. |
| if (body_markup) |
| body += "<b>" + title + "</b> " + message; |
| else |
| body += title + " - " + message; |
| } |
| } |
| } |
| writer.AppendString(body); |
| |
| // Even-indexed elements in this vector are action IDs passed back to |
| // us in OnActionInvoked(). Odd-indexed ones contain the button text. |
| std::vector<std::string> actions; |
| if (base::ContainsKey(capabilities_, kCapabilityActions)) { |
| data->action_start = data->action_end; |
| for (const auto& button_info : notification->buttons()) { |
| // FDO notification buttons can contain either an icon or a label, |
| // but not both, and the type of all buttons must be the same (all |
| // labels or all icons), so always use labels. |
| const std::string id = base::SizeTToString(data->action_end++); |
| const std::string label = base::UTF16ToUTF8(button_info.title); |
| actions.push_back(id); |
| actions.push_back(label); |
| } |
| if (notification->clickable()) { |
| // Special case: the pair ("default", "") will not add a button, |
| // but instead makes the entire notification clickable. |
| actions.push_back(kDefaultButtonId); |
| actions.push_back(""); |
| } |
| // Always add a settings button. |
| actions.push_back(kSettingsButtonId); |
| actions.push_back( |
| l10n_util::GetStringUTF8(IDS_NOTIFICATION_BUTTON_SETTINGS)); |
| } |
| writer.AppendArrayOfStrings(actions); |
| |
| dbus::MessageWriter hints_writer(nullptr); |
| writer.OpenArray("{sv}", &hints_writer); |
| dbus::MessageWriter urgency_writer(nullptr); |
| hints_writer.OpenDictEntry(&urgency_writer); |
| urgency_writer.AppendString("urgency"); |
| urgency_writer.AppendVariantOfUint32( |
| NotificationPriorityToFdoUrgency(notification->priority())); |
| hints_writer.CloseContainer(&urgency_writer); |
| |
| std::unique_ptr<base::Environment> env = base::Environment::Create(); |
| base::FilePath desktop_file( |
| shell_integration_linux::GetDesktopName(env.get())); |
| const char kDesktopFileSuffix[] = ".desktop"; |
| DCHECK(base::EndsWith(desktop_file.value(), kDesktopFileSuffix, |
| base::CompareCase::SENSITIVE)); |
| desktop_file = desktop_file.RemoveFinalExtension(); |
| dbus::MessageWriter desktop_entry_writer(nullptr); |
| hints_writer.OpenDictEntry(&desktop_entry_writer); |
| desktop_entry_writer.AppendString("desktop-entry"); |
| desktop_entry_writer.AppendVariantOfString(desktop_file.value()); |
| hints_writer.CloseContainer(&desktop_entry_writer); |
| |
| std::unique_ptr<ResourceFile> icon_file = |
| WriteDataToTmpFile(notification->icon().As1xPNGBytes()); |
| if (icon_file) { |
| dbus::MessageWriter image_path_writer(nullptr); |
| hints_writer.OpenDictEntry(&image_path_writer); |
| image_path_writer.AppendString( |
| spec_version_ == base::Version(std::vector<uint32_t>{1, 1}) |
| ? "image_path" |
| : "image-path"); |
| image_path_writer.AppendVariantOfString(icon_file->file_path().value()); |
| hints_writer.CloseContainer(&image_path_writer); |
| data->resource_files.push_back(std::move(icon_file)); |
| } |
| |
| writer.CloseContainer(&hints_writer); |
| |
| const int32_t kExpireTimeoutDefault = -1; |
| const int32_t kExpireTimeoutNever = 0; |
| writer.AppendInt32(notification->never_timeout() ? kExpireTimeoutNever |
| : kExpireTimeoutDefault); |
| |
| std::unique_ptr<dbus::Response> response = |
| notification_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); |
| if (response) { |
| dbus::MessageReader reader(response.get()); |
| reader.PopUint32(&data->dbus_id); |
| } |
| if (!data->dbus_id) { |
| // There was some sort of error with creating the notification. |
| notifications_.erase(data); |
| } |
| } |
| |
| // Makes the "CloseNotification" call to D-Bus. |
| void CloseOnTaskRunner(const std::string& profile_id, |
| const std::string& notification_id) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| std::vector<NotificationData*> to_erase; |
| for (const auto& pair : notifications_) { |
| NotificationData* data = pair.first; |
| if (data->notification_id == notification_id && |
| data->profile_id == profile_id) { |
| dbus::MethodCall method_call(kFreedesktopNotificationsName, |
| kMethodCloseNotification); |
| dbus::MessageWriter writer(&method_call); |
| writer.AppendUint32(data->dbus_id); |
| notification_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); |
| to_erase.push_back(data); |
| } |
| } |
| for (NotificationData* data : to_erase) |
| notifications_.erase(data); |
| } |
| |
| void GetDisplayedOnTaskRunner( |
| const std::string& profile_id, |
| bool incognito, |
| const GetDisplayedNotificationsCallback& callback) const { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| auto displayed = base::MakeUnique<std::set<std::string>>(); |
| for (const auto& pair : notifications_) { |
| NotificationData* data = pair.first; |
| if (data->profile_id == profile_id && data->is_incognito == incognito) |
| displayed->insert(data->notification_id); |
| } |
| PostTaskToUiThread(base::BindOnce(callback, std::move(displayed), true)); |
| } |
| |
| NotificationData* FindNotificationData(const std::string& notification_id, |
| const std::string& profile_id, |
| bool is_incognito) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| for (const auto& pair : notifications_) { |
| NotificationData* data = pair.first; |
| if (data->notification_id == notification_id && |
| data->profile_id == profile_id && |
| data->is_incognito == is_incognito) { |
| return data; |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| NotificationData* FindNotificationDataWithDBusId(uint32_t dbus_id) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| DCHECK(dbus_id); |
| for (const auto& pair : notifications_) { |
| NotificationData* data = pair.first; |
| if (data->dbus_id == dbus_id) |
| return data; |
| } |
| |
| return nullptr; |
| } |
| |
| void ForwardNotificationOperation(NotificationData* data, |
| NotificationCommon::Operation operation, |
| int action_index) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| PostTaskToUiThread(base::BindOnce( |
| ForwardNotificationOperationOnUiThread, operation, |
| data->notification_type, data->origin_url.spec(), data->notification_id, |
| action_index, data->profile_id, data->is_incognito)); |
| } |
| |
| void OnActionInvoked(dbus::Signal* signal) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| dbus::MessageReader reader(signal); |
| uint32_t dbus_id; |
| if (!reader.PopUint32(&dbus_id) || !dbus_id) |
| return; |
| std::string action; |
| if (!reader.PopString(&action)) |
| return; |
| |
| NotificationData* data = FindNotificationDataWithDBusId(dbus_id); |
| if (!data) |
| return; |
| |
| if (action == kDefaultButtonId) { |
| ForwardNotificationOperation(data, NotificationCommon::CLICK, -1); |
| } else if (action == kSettingsButtonId) { |
| ForwardNotificationOperation(data, NotificationCommon::SETTINGS, -1); |
| } else { |
| size_t id; |
| if (!base::StringToSizeT(action, &id)) |
| return; |
| size_t n_buttons = data->action_end - data->action_start; |
| size_t id_zero_based = id - data->action_start; |
| if (id_zero_based >= n_buttons) |
| return; |
| ForwardNotificationOperation(data, NotificationCommon::CLICK, |
| id_zero_based); |
| } |
| } |
| |
| void OnNotificationClosed(dbus::Signal* signal) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| dbus::MessageReader reader(signal); |
| uint32_t dbus_id; |
| if (!reader.PopUint32(&dbus_id) || !dbus_id) |
| return; |
| |
| NotificationData* data = FindNotificationDataWithDBusId(dbus_id); |
| if (!data) |
| return; |
| |
| ForwardNotificationOperation(data, NotificationCommon::CLOSE, -1); |
| notifications_.erase(data); |
| } |
| |
| // Called once the connection has been set up (or not). |success| |
| // indicates the connection is ready to use. |
| void OnConnectionInitializationFinishedOnUiThread(bool success) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| connected_ = success; |
| for (auto& callback : on_connected_callbacks_) |
| std::move(callback).Run(success); |
| on_connected_callbacks_.clear(); |
| if (!success) |
| CleanUp(); |
| } |
| |
| void OnConnectionInitializationFinishedOnTaskRunner( |
| ConnectionInitializationStatusCode status) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| UMA_HISTOGRAM_ENUMERATION( |
| "Notifications.Linux.BridgeInitializationStatus", |
| static_cast<int>(status), |
| static_cast<int>(ConnectionInitializationStatusCode::NUM_ITEMS)); |
| PostTaskToUiThread(base::BindOnce( |
| &NotificationPlatformBridgeLinuxImpl:: |
| OnConnectionInitializationFinishedOnUiThread, |
| this, status == ConnectionInitializationStatusCode::SUCCESS)); |
| } |
| |
| void OnSignalConnected(const std::string& interface_name, |
| const std::string& signal_name, |
| bool success) { |
| DCHECK(task_runner_->RunsTasksOnCurrentThread()); |
| if (!success) { |
| OnConnectionInitializationFinishedOnTaskRunner( |
| ConnectionInitializationStatusCode::COULD_NOT_CONNECT_TO_SIGNALS); |
| return; |
| } |
| connected_signals_barrier_.Run(); |
| } |
| |
| void RecordMetricsForCapabilities() { |
| // Histogram macros must be called with the same name for each |
| // callsite, so we can't roll the below into a nice loop. |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.ActionIcons", |
| base::ContainsKey(capabilities_, kCapabilityActionIcons)); |
| UMA_HISTOGRAM_BOOLEAN("Notifications.Freedesktop.Capabilities.Actions", |
| base::ContainsKey(capabilities_, kCapabilityActions)); |
| UMA_HISTOGRAM_BOOLEAN("Notifications.Freedesktop.Capabilities.Body", |
| base::ContainsKey(capabilities_, kCapabilityBody)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.BodyHyperlinks", |
| base::ContainsKey(capabilities_, kCapabilityBodyHyperlinks)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.BodyImages", |
| base::ContainsKey(capabilities_, kCapabilityBodyImages)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.BodyMarkup", |
| base::ContainsKey(capabilities_, kCapabilityBodyMarkup)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.IconMulti", |
| base::ContainsKey(capabilities_, kCapabilityIconMulti)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.IconStatic", |
| base::ContainsKey(capabilities_, kCapabilityIconStatic)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Freedesktop.Capabilities.Persistence", |
| base::ContainsKey(capabilities_, kCapabilityPersistence)); |
| UMA_HISTOGRAM_BOOLEAN("Notifications.Freedesktop.Capabilities.Sound", |
| base::ContainsKey(capabilities_, kCapabilitySound)); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // Members used only on the UI thread. |
| |
| scoped_refptr<base::SequencedTaskRunner> task_runner_; |
| |
| content::NotificationRegistrar registrar_; |
| |
| // State necessary for OnConnectionInitializationFinished() and |
| // SetReadyCallback(). |
| base::Optional<bool> connected_; |
| std::vector<NotificationBridgeReadyCallback> on_connected_callbacks_; |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // Members used only on the task runner thread. |
| |
| scoped_refptr<dbus::Bus> bus_; |
| |
| dbus::ObjectProxy* notification_proxy_ = nullptr; |
| |
| std::unordered_set<std::string> capabilities_; |
| |
| base::Version spec_version_; |
| |
| base::Closure connected_signals_barrier_; |
| |
| // A std::set<std::unique_ptr<T>> doesn't work well because |
| // eg. std::set::erase(T) would require a std::unique_ptr<T> |
| // argument, so the data would get double-destructed. |
| template <typename T> |
| using UnorderedUniqueSet = std::unordered_map<T*, std::unique_ptr<T>>; |
| |
| UnorderedUniqueSet<NotificationData> notifications_; |
| |
| DISALLOW_COPY_AND_ASSIGN(NotificationPlatformBridgeLinuxImpl); |
| }; |
| |
| NotificationPlatformBridgeLinux::NotificationPlatformBridgeLinux() |
| : NotificationPlatformBridgeLinux(nullptr) {} |
| |
| NotificationPlatformBridgeLinux::NotificationPlatformBridgeLinux( |
| scoped_refptr<dbus::Bus> bus) |
| : impl_(new NotificationPlatformBridgeLinuxImpl(bus)) { |
| impl_->Init(); |
| } |
| |
| NotificationPlatformBridgeLinux::~NotificationPlatformBridgeLinux() = default; |
| |
| void NotificationPlatformBridgeLinux::Display( |
| NotificationCommon::Type notification_type, |
| const std::string& notification_id, |
| const std::string& profile_id, |
| bool is_incognito, |
| const Notification& notification) { |
| impl_->Display(notification_type, notification_id, profile_id, is_incognito, |
| notification); |
| } |
| |
| void NotificationPlatformBridgeLinux::Close( |
| const std::string& profile_id, |
| const std::string& notification_id) { |
| impl_->Close(profile_id, notification_id); |
| } |
| |
| void NotificationPlatformBridgeLinux::GetDisplayed( |
| const std::string& profile_id, |
| bool incognito, |
| const GetDisplayedNotificationsCallback& callback) const { |
| impl_->GetDisplayed(profile_id, incognito, callback); |
| } |
| |
| void NotificationPlatformBridgeLinux::SetReadyCallback( |
| NotificationBridgeReadyCallback callback) { |
| impl_->SetReadyCallback(std::move(callback)); |
| } |
| |
| void NotificationPlatformBridgeLinux::CleanUp() { |
| impl_->CleanUp(); |
| } |