// 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 #include #include #include #include #include #include #include #include "base/barrier_closure.h" #include "base/files/file_path_watcher.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 "chrome/browser/browser_process.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/dbus/dbus_thread_linux.h" #include "chrome/browser/notifications/notification_display_service.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/chrome_unscaled_resources.h" #include "chrome/grit/chromium_strings.h" #include "chrome/grit/generated_resources.h" #include "components/url_formatter/elide_url.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 "net/base/escape.h" #include "net/base/registry_controlled_domains/registry_controlled_domain.h" #include "skia/ext/image_operations.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image_skia.h" #include "ui/message_center/notification.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"; // 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"; // Max image size; specified in the FDO notification specification. const int kMaxImageWidth = 200; const int kMaxImageHeight = 100; // Notification on-screen time, in milliseconds. const int32_t kExpireTimeout = 25000; // The maximum amount of characters for displaying the full origin path. const size_t kMaxAllowedOriginLength = 28; // 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, // DEPRECATED NUM_ITEMS }; int ClampInt(int value, int low, int hi) { return std::max(std::min(value, hi), low); } base::string16 CreateNotificationTitle( const message_center::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; } void EscapeUnsafeCharacters(std::string* message) { // Canonical's notification development guidelines recommends only // escaping the '&', '<', and '>' characters: // https://wiki.ubuntu.com/NotificationDevelopmentGuidelines base::ReplaceChars(*message, "&", "&", message); base::ReplaceChars(*message, "<", "<", message); base::ReplaceChars(*message, ">", ">", message); } 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; } } // Constrain |image|'s size to |kMaxImageWidth|x|kMaxImageHeight|. If // the image does not need to be resized, or the image is empty, // returns |image| directly. gfx::Image ResizeImageToFdoMaxSize(const gfx::Image& image) { DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); if (image.IsEmpty()) return image; int width = image.Width(); int height = image.Height(); if (width <= kMaxImageWidth && height <= kMaxImageHeight) { return image; } const SkBitmap* image_bitmap = image.ToSkBitmap(); double scale = std::min(static_cast(kMaxImageWidth) / width, static_cast(kMaxImageHeight) / height); width = ClampInt(scale * width, 1, kMaxImageWidth); height = ClampInt(scale * height, 1, kMaxImageHeight); return gfx::Image( gfx::ImageSkia::CreateFrom1xBitmap(skia::ImageOperations::Resize( *image_bitmap, skia::ImageOperations::RESIZE_LANCZOS3, width, height))); } // 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 GURL& origin, const std::string& notification_id, const base::Optional& action_index, const base::Optional& reply, const base::Optional& by_user, Profile* profile) { if (!profile) return; auto* display_service = NotificationDisplayServiceFactory::GetForProfile(profile); display_service->ProcessNotificationOperation(operation, notification_type, origin, notification_id, action_index, reply, by_user); } void ForwardNotificationOperationOnUiThread( NotificationCommon::Operation operation, NotificationCommon::Type notification_type, const GURL& origin, const std::string& notification_id, const base::Optional& action_index, const base::Optional& by_user, 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::nullopt /* reply */, by_user)); } class ResourceFile { public: explicit ResourceFile(const base::FilePath& file_path) : file_path_(file_path) { DCHECK(!file_path.empty()); DCHECK(file_path.IsAbsolute()); } ~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 WriteDataToTmpFile( const scoped_refptr& 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 = std::make_unique(file_path); if (base::WriteFile(file_path, data->front_as(), 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 { public: explicit NotificationPlatformBridgeLinuxImpl(scoped_refptr bus) : bus_(bus) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); task_runner_ = chrome::GetDBusTaskRunner(); 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() { product_logo_png_bytes_ = gfx::Image(*ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( IDR_PRODUCT_LOGO_256)) .As1xPNGBytes(); PostTaskToTaskRunnerThread(base::BindOnce( &NotificationPlatformBridgeLinuxImpl::InitOnTaskRunner, this)); } void Display( NotificationCommon::Type notification_type, const std::string& profile_id, bool is_incognito, const message_center::Notification& notification, std::unique_ptr metadata) override { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); // Make a deep copy of the notification as its resources cannot safely // be passed between threads. auto notification_copy = message_center::Notification::DeepCopy( notification, body_images_supported_.value(), /*include_small_image=*/false, /*include_icon_images=*/false); PostTaskToTaskRunnerThread(base::BindOnce( &NotificationPlatformBridgeLinuxImpl::DisplayOnTaskRunner, this, notification_type, 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; 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; 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 // NotificationDisplayService. 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> resource_files; }; ~NotificationPlatformBridgeLinuxImpl() override { DCHECK(clean_up_on_task_runner_called_); } 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 SetBodyImagesSupported(bool body_images_supported) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); body_images_supported_ = body_images_supported; } void PostTaskToUiThread(base::OnceClosure closure) const { DCHECK(task_runner_->RunsTasksInCurrentSequence()); 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_->RunsTasksInCurrentSequence()); // |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_ = base::MakeRefCounted(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 capabilities_response = notification_proxy_->CallMethodAndBlock( &get_capabilities_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT); if (capabilities_response) { dbus::MessageReader reader(capabilities_response.get()); std::vector capabilities; reader.PopArrayOfStrings(&capabilities); for (const std::string& capability : capabilities) capabilities_.insert(capability); } RecordMetricsForCapabilities(); if (!base::ContainsKey(capabilities_, kCapabilityBody) || !base::ContainsKey(capabilities_, kCapabilityActions)) { OnConnectionInitializationFinishedOnTaskRunner( ConnectionInitializationStatusCode::MISSING_REQUIRED_CAPABILITIES); return; } PostTaskToUiThread(base::BindOnce( &NotificationPlatformBridgeLinuxImpl::SetBodyImagesSupported, this, base::ContainsKey(capabilities_, kCapabilityBodyImages))); 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_->RunsTasksInCurrentSequence()); if (bus_) bus_->ShutdownAndBlock(); bus_ = nullptr; notification_proxy_ = nullptr; product_logo_png_bytes_ = nullptr; product_logo_file_.reset(); product_logo_file_watcher_.reset(); notifications_.clear(); clean_up_on_task_runner_called_ = true; } // Makes the "Notify" call to D-Bus. void DisplayOnTaskRunner( NotificationCommon::Type notification_type, const std::string& profile_id, bool is_incognito, std::unique_ptr notification) { DCHECK(task_runner_->RunsTasksInCurrentSequence()); 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 writer.AppendString(l10n_util::GetStringUTF8(IDS_PRODUCT_NAME)); writer.AppendUint32(data->dbus_id); // app_icon if (!product_logo_file_) { RewriteProductLogoFile(); } writer.AppendString( product_logo_file_ ? "file://" + product_logo_file_->file_path().value() : ""); writer.AppendString( base::UTF16ToUTF8(CreateNotificationTitle(*notification))); std::ostringstream body; if (base::ContainsKey(capabilities_, kCapabilityBody)) { const bool body_markup = base::ContainsKey(capabilities_, kCapabilityBodyMarkup); if (notification->UseOriginAsContextMessage()) { std::string url_display_text = base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay( notification->origin_url(), url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)); if (url_display_text.size() > kMaxAllowedOriginLength) { std::string domain_and_registry = net::registry_controlled_domains::GetDomainAndRegistry( notification->origin_url(), net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); // localhost, raw IPs etc. are not handled by GetDomainAndRegistry. if (!domain_and_registry.empty()) { url_display_text = domain_and_registry; } } EscapeUnsafeCharacters(&url_display_text); if (body_markup && base::ContainsKey(capabilities_, kCapabilityBodyHyperlinks)) { body << "origin_url().spec()) << "\">" << url_display_text << "\n\n"; } else { body << url_display_text << "\n\n"; } } else if (!notification->context_message().empty()) { std::string context = base::UTF16ToUTF8(notification->context_message()); if (body_markup) EscapeUnsafeCharacters(&context); body << context << "\n\n"; } std::string message = base::UTF16ToUTF8(notification->message()); if (body_markup) EscapeUnsafeCharacters(&message); if (!message.empty()) body << message << "\n"; if (notification->type() == message_center::NOTIFICATION_TYPE_MULTIPLE) { for (const auto& item : notification->items()) { 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 << "" << title << " " << message << "\n"; else body << title << " - " << message << "\n"; } } else if (notification->type() == message_center::NOTIFICATION_TYPE_IMAGE && base::ContainsKey(capabilities_, kCapabilityBodyImages)) { std::unique_ptr image_file = WriteDataToTmpFile( ResizeImageToFdoMaxSize(notification->image()).As1xPNGBytes()); if (image_file) { body << "file_path().value()) << "\" alt=\"\"/>\n"; data->resource_files.push_back(std::move(image_file)); } } } std::string body_str = body.str(); base::TrimString(body_str, "\n", &body_str); writer.AppendString(body_str); // Even-indexed elements in this vector are action IDs passed back to // us in OnActionInvoked(). Odd-indexed ones contain the button text. std::vector 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); } // Special case: the id "default" will not add a button, but // instead makes the entire notification clickable. actions.push_back(kDefaultButtonId); actions.push_back("Activate"); // Always add a settings button for web notifications. if (notification_type != NotificationCommon::EXTENSION) { 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); if (notification->silent()) { dbus::MessageWriter suppress_sound_writer(nullptr); hints_writer.OpenDictEntry(&suppress_sound_writer); suppress_sound_writer.AppendString("suppress-sound"); suppress_sound_writer.AppendVariantOfBool(true); hints_writer.CloseContainer(&suppress_sound_writer); } std::unique_ptr 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 icon_file = WriteDataToTmpFile(notification->icon().As1xPNGBytes()); if (icon_file) { for (const std::string& hint_name : {"image_path", "image-path"}) { dbus::MessageWriter image_path_writer(nullptr); hints_writer.OpenDictEntry(&image_path_writer); image_path_writer.AppendString(hint_name); 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 : base::ContainsKey(capabilities_, kCapabilityPersistence) ? kExpireTimeoutDefault : kExpireTimeout); std::unique_ptr 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_->RunsTasksInCurrentSequence()); std::vector 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_->RunsTasksInCurrentSequence()); auto displayed = std::make_unique>(); 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_->RunsTasksInCurrentSequence()); 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_->RunsTasksInCurrentSequence()); 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, const base::Optional& action_index, const base::Optional& by_user) { DCHECK(task_runner_->RunsTasksInCurrentSequence()); PostTaskToUiThread(base::BindOnce( ForwardNotificationOperationOnUiThread, operation, data->notification_type, data->origin_url, data->notification_id, action_index, by_user, data->profile_id, data->is_incognito)); } void OnActionInvoked(dbus::Signal* signal) { DCHECK(task_runner_->RunsTasksInCurrentSequence()); 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, base::nullopt /* action_index */, base::nullopt /* by_user */); } else if (action == kSettingsButtonId) { ForwardNotificationOperation(data, NotificationCommon::SETTINGS, base::nullopt /* action_index */, base::nullopt /* by_user */); } 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, base::nullopt /* by_user */); } } void OnNotificationClosed(dbus::Signal* signal) { DCHECK(task_runner_->RunsTasksInCurrentSequence()); dbus::MessageReader reader(signal); uint32_t dbus_id; if (!reader.PopUint32(&dbus_id) || !dbus_id) return; NotificationData* data = FindNotificationDataWithDBusId(dbus_id); if (!data) return; // TODO(peter): Can we support |by_user| appropriately here? ForwardNotificationOperation(data, NotificationCommon::CLOSE, base::nullopt /* action_index */, true /* by_user */); 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_->RunsTasksInCurrentSequence()); UMA_HISTOGRAM_ENUMERATION( "Notifications.Linux.BridgeInitializationStatus", static_cast(status), static_cast(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_->RunsTasksInCurrentSequence()); if (!success) { OnConnectionInitializationFinishedOnTaskRunner( ConnectionInitializationStatusCode::COULD_NOT_CONNECT_TO_SIGNALS); return; } connected_signals_barrier_.Run(); } void OnProductLogoFileChanged(const base::FilePath& path, bool error) { // |error| should always be false on Linux. DCHECK(!error); // This callback runs whenever the file is deleted or modified. // In either case, we want to rewrite the file. product_logo_file_.reset(); product_logo_file_watcher_.reset(); } 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)); } void RewriteProductLogoFile() { product_logo_file_watcher_.reset(); product_logo_file_ = WriteDataToTmpFile(product_logo_png_bytes_); if (!product_logo_file_) return; // Temporary files may periodically get cleaned up on Linux. // Watch for file deletion and rewrite the file in case we have a // long-running Chrome process. product_logo_file_watcher_ = std::make_unique(); if (!product_logo_file_watcher_->Watch( product_logo_file_->file_path(), false, base::Bind( &NotificationPlatformBridgeLinuxImpl::OnProductLogoFileChanged, this))) { product_logo_file_.reset(); product_logo_file_watcher_.reset(); } } ////////////////////////////////////////////////////////////////////////////// // Members used only on the UI thread. scoped_refptr task_runner_; content::NotificationRegistrar registrar_; // State necessary for OnConnectionInitializationFinished() and // SetReadyCallback(). base::Optional connected_; std::vector on_connected_callbacks_; // Notification servers very rarely have the 'body-images' // capability, so try to avoid an image copy if possible. base::Optional body_images_supported_; ////////////////////////////////////////////////////////////////////////////// // Members used only on the task runner thread. scoped_refptr bus_; dbus::ObjectProxy* notification_proxy_ = nullptr; std::unordered_set capabilities_; base::Closure connected_signals_barrier_; scoped_refptr product_logo_png_bytes_; std::unique_ptr product_logo_file_; std::unique_ptr product_logo_file_watcher_; // A std::set> doesn't work well because // eg. std::set::erase(T) would require a std::unique_ptr // argument, so the data would get double-destructed. template using UnorderedUniqueSet = std::unordered_map>; UnorderedUniqueSet notifications_; bool clean_up_on_task_runner_called_ = false; DISALLOW_COPY_AND_ASSIGN(NotificationPlatformBridgeLinuxImpl); }; NotificationPlatformBridgeLinux::NotificationPlatformBridgeLinux() : NotificationPlatformBridgeLinux(nullptr) {} NotificationPlatformBridgeLinux::NotificationPlatformBridgeLinux( scoped_refptr bus) : impl_(new NotificationPlatformBridgeLinuxImpl(bus)) { impl_->Init(); } NotificationPlatformBridgeLinux::~NotificationPlatformBridgeLinux() = default; void NotificationPlatformBridgeLinux::Display( NotificationCommon::Type notification_type, const std::string& profile_id, bool is_incognito, const message_center::Notification& notification, std::unique_ptr metadata) { impl_->Display(notification_type, profile_id, is_incognito, notification, std::move(metadata)); } 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(); }