| // Copyright 2013 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/printing/cloud_print/privet_notifications.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/stl_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/local_discovery/service_discovery_shared_client.h" |
| #include "chrome/browser/notifications/notification_display_service.h" |
| #include "chrome/browser/notifications/notification_handler.h" |
| #include "chrome/browser/printing/cloud_print/privet_device_lister_impl.h" |
| #include "chrome/browser/printing/cloud_print/privet_http_asynchronous_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/signin/signin_manager_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/webui/local_discovery/local_discovery_ui_handler.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/core/browser/signin_manager_base.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/net_buildflags.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "ui/message_center/public/cpp/notifier_id.h" |
| |
| #if BUILDFLAG(ENABLE_MDNS) |
| #include "chrome/browser/printing/cloud_print/privet_traffic_detector.h" |
| #endif |
| |
| namespace cloud_print { |
| |
| namespace { |
| |
| const int kTenMinutesInSeconds = 600; |
| const char kPrivetInfoKeyUptime[] = "uptime"; |
| const char kPrivetNotificationID[] = "privet_notification"; |
| const char kPrivetNotificationOriginUrl[] = "chrome://devices"; |
| const int kStartDelaySeconds = 5; |
| |
| enum PrivetNotificationsEvent { |
| PRIVET_SERVICE_STARTED, |
| PRIVET_LISTER_STARTED, |
| PRIVET_DEVICE_CHANGED, |
| PRIVET_INFO_DONE, |
| PRIVET_NOTIFICATION_SHOWN, |
| PRIVET_NOTIFICATION_CANCELED, |
| PRIVET_NOTIFICATION_CLICKED, |
| PRIVET_DISABLE_NOTIFICATIONS_CLICKED, |
| PRIVET_EVENT_MAX, |
| }; |
| |
| void ReportPrivetUmaEvent(PrivetNotificationsEvent privet_event) { |
| UMA_HISTOGRAM_ENUMERATION("LocalDiscovery.PrivetNotificationsEvent", |
| privet_event, PRIVET_EVENT_MAX); |
| } |
| |
| } // namespace |
| |
| PrivetNotificationsListener::PrivetNotificationsListener( |
| std::unique_ptr<PrivetHTTPAsynchronousFactory> privet_http_factory, |
| Delegate* delegate) |
| : delegate_(delegate), devices_active_(0) { |
| privet_http_factory_.swap(privet_http_factory); |
| } |
| |
| PrivetNotificationsListener::~PrivetNotificationsListener() { |
| } |
| |
| void PrivetNotificationsListener::DeviceChanged( |
| const std::string& name, |
| const DeviceDescription& description) { |
| ReportPrivetUmaEvent(PRIVET_DEVICE_CHANGED); |
| DeviceContextMap::iterator it = devices_seen_.find(name); |
| if (it != devices_seen_.end()) { |
| if (!description.id.empty() && // Device is registered |
| it->second->notification_may_be_active) { |
| it->second->notification_may_be_active = false; |
| devices_active_--; |
| NotifyDeviceRemoved(); |
| } |
| return; // Already saw this device. |
| } |
| |
| std::unique_ptr<DeviceContext>& device_context = devices_seen_[name]; |
| device_context.reset(new DeviceContext); |
| device_context->notification_may_be_active = false; |
| device_context->registered = !description.id.empty(); |
| |
| if (device_context->registered) |
| return; |
| |
| device_context->privet_http_resolution = |
| privet_http_factory_->CreatePrivetHTTP(name); |
| device_context->privet_http_resolution->Start( |
| description.address, |
| base::Bind(&PrivetNotificationsListener::CreateInfoOperation, |
| base::Unretained(this))); |
| } |
| |
| void PrivetNotificationsListener::CreateInfoOperation( |
| std::unique_ptr<PrivetHTTPClient> http_client) { |
| // Do nothing if resolution fails. |
| if (!http_client) |
| return; |
| |
| std::string name = http_client->GetName(); |
| DeviceContextMap::iterator it = devices_seen_.find(name); |
| if (it == devices_seen_.end()) |
| return; |
| |
| DeviceContext* device = it->second.get(); |
| device->privet_http.swap(http_client); |
| device->info_operation = device->privet_http->CreateInfoOperation( |
| base::Bind(&PrivetNotificationsListener::OnPrivetInfoDone, |
| base::Unretained(this), |
| device)); |
| device->info_operation->Start(); |
| } |
| |
| void PrivetNotificationsListener::OnPrivetInfoDone( |
| DeviceContext* device, |
| const base::DictionaryValue* json_value) { |
| int uptime; |
| |
| if (!json_value || |
| !json_value->GetInteger(kPrivetInfoKeyUptime, &uptime) || |
| uptime > kTenMinutesInSeconds) { |
| return; |
| } |
| |
| DCHECK(!device->notification_may_be_active); |
| device->notification_may_be_active = true; |
| devices_active_++; |
| delegate_->PrivetNotify(devices_active_, true); |
| } |
| |
| void PrivetNotificationsListener::DeviceRemoved(const std::string& name) { |
| DeviceContextMap::iterator it = devices_seen_.find(name); |
| if (it == devices_seen_.end()) |
| return; |
| |
| DeviceContext* device = it->second.get(); |
| device->info_operation.reset(); |
| device->privet_http_resolution.reset(); |
| if (!device->notification_may_be_active) |
| return; |
| |
| device->notification_may_be_active = false; |
| devices_active_--; |
| NotifyDeviceRemoved(); |
| } |
| |
| void PrivetNotificationsListener::DeviceCacheFlushed() { |
| for (const auto& it : devices_seen_) { |
| DeviceContext* device = it.second.get(); |
| device->info_operation.reset(); |
| device->privet_http_resolution.reset(); |
| device->notification_may_be_active = false; |
| } |
| |
| devices_active_ = 0; |
| NotifyDeviceRemoved(); |
| } |
| |
| void PrivetNotificationsListener::NotifyDeviceRemoved() { |
| if (devices_active_ == 0) { |
| delegate_->PrivetRemoveNotification(); |
| } else { |
| delegate_->PrivetNotify(devices_active_, false); |
| } |
| } |
| |
| PrivetNotificationsListener::DeviceContext::DeviceContext() { |
| } |
| |
| PrivetNotificationsListener::DeviceContext::~DeviceContext() { |
| } |
| |
| PrivetNotificationService::PrivetNotificationService( |
| content::BrowserContext* profile) |
| : profile_(profile) { |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, base::BindOnce(&PrivetNotificationService::Start, AsWeakPtr()), |
| base::TimeDelta::FromSeconds(kStartDelaySeconds + |
| base::RandInt(0, kStartDelaySeconds / 4))); |
| } |
| |
| PrivetNotificationService::~PrivetNotificationService() { |
| #if BUILDFLAG(ENABLE_MDNS) |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (traffic_detector_) |
| traffic_detector_->Stop(); |
| #endif |
| } |
| |
| void PrivetNotificationService::DeviceChanged( |
| const std::string& name, |
| const DeviceDescription& description) { |
| privet_notifications_listener_->DeviceChanged(name, description); |
| } |
| |
| void PrivetNotificationService::DeviceRemoved(const std::string& name) { |
| privet_notifications_listener_->DeviceRemoved(name); |
| } |
| |
| void PrivetNotificationService::DeviceCacheFlushed() { |
| privet_notifications_listener_->DeviceCacheFlushed(); |
| } |
| |
| // static |
| bool PrivetNotificationService::IsEnabled() { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| return !command_line->HasSwitch( |
| switches::kDisableDeviceDiscoveryNotifications); |
| } |
| |
| // static |
| bool PrivetNotificationService::IsForced() { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| return command_line->HasSwitch(switches::kEnableDeviceDiscoveryNotifications); |
| } |
| |
| void PrivetNotificationService::PrivetNotify(int devices_active, |
| bool added) { |
| DCHECK_GT(devices_active, 0); |
| |
| NotificationDisplayService::GetForProfile( |
| Profile::FromBrowserContext(profile_)) |
| ->GetDisplayed(base::Bind(&PrivetNotificationService::AddNotification, |
| AsWeakPtr(), devices_active, added)); |
| } |
| |
| void PrivetNotificationService::AddNotification( |
| int devices_active, |
| bool device_added, |
| std::unique_ptr<std::set<std::string>> displayed_notifications, |
| bool supports_synchronization) { |
| // If the UI is already open or a device was removed, we'll update the |
| // existing notification but not add a new one. |
| const bool notification_exists = |
| base::ContainsKey(*displayed_notifications, kPrivetNotificationID); |
| const bool add_new_notification = |
| device_added && |
| !local_discovery::LocalDiscoveryUIHandler::GetHasVisible(); |
| if (!notification_exists && !add_new_notification) |
| return; |
| |
| message_center::RichNotificationData rich_notification_data; |
| rich_notification_data.buttons.push_back( |
| message_center::ButtonInfo(l10n_util::GetStringUTF16( |
| IDS_LOCAL_DISCOVERY_NOTIFICATION_BUTTON_PRINTER))); |
| rich_notification_data.buttons.push_back( |
| message_center::ButtonInfo(l10n_util::GetStringUTF16( |
| IDS_LOCAL_DISCOVERY_NOTIFICATIONS_DISABLE_BUTTON_LABEL))); |
| |
| base::string16 title = l10n_util::GetPluralStringFUTF16( |
| IDS_LOCAL_DISCOVERY_NOTIFICATION_TITLE_PRINTER, devices_active); |
| base::string16 body = l10n_util::GetPluralStringFUTF16( |
| IDS_LOCAL_DISCOVERY_NOTIFICATION_CONTENTS_PRINTER, devices_active); |
| base::string16 product_name = |
| l10n_util::GetStringUTF16(IDS_LOCAL_DISCOVERY_SERVICE_NAME_PRINTER); |
| |
| Profile* profile = Profile::FromBrowserContext(profile_); |
| message_center::Notification notification( |
| message_center::NOTIFICATION_TYPE_SIMPLE, kPrivetNotificationID, title, |
| body, |
| ui::ResourceBundle::GetSharedInstance().GetImageNamed( |
| IDR_LOCAL_DISCOVERY_CLOUDPRINT_ICON), |
| product_name, GURL(kPrivetNotificationOriginUrl), |
| message_center::NotifierId(message_center::NotifierId::SYSTEM_COMPONENT, |
| kPrivetNotificationID), |
| rich_notification_data, CreateNotificationDelegate(profile)); |
| |
| if (add_new_notification) |
| ReportPrivetUmaEvent(PRIVET_NOTIFICATION_SHOWN); |
| |
| NotificationDisplayService::GetForProfile( |
| Profile::FromBrowserContext(profile_)) |
| ->Display(NotificationHandler::Type::TRANSIENT, notification); |
| } |
| |
| void PrivetNotificationService::PrivetRemoveNotification() { |
| ReportPrivetUmaEvent(PRIVET_NOTIFICATION_CANCELED); |
| NotificationDisplayService::GetForProfile( |
| Profile::FromBrowserContext(profile_)) |
| ->Close(NotificationHandler::Type::TRANSIENT, kPrivetNotificationID); |
| } |
| |
| void PrivetNotificationService::Start() { |
| #if defined(CHROMEOS) |
| SigninManagerBase* signin_manager = |
| SigninManagerFactory::GetForProfileIfExists( |
| Profile::FromBrowserContext(profile_)); |
| |
| if (!signin_manager || !signin_manager->IsAuthenticated()) |
| return; |
| #endif |
| |
| enable_privet_notification_member_.Init( |
| prefs::kLocalDiscoveryNotificationsEnabled, |
| Profile::FromBrowserContext(profile_)->GetPrefs(), |
| base::Bind(&PrivetNotificationService::OnNotificationsEnabledChanged, |
| base::Unretained(this))); |
| OnNotificationsEnabledChanged(); |
| } |
| |
| void PrivetNotificationService::OnNotificationsEnabledChanged() { |
| #if BUILDFLAG(ENABLE_MDNS) |
| if (IsForced()) { |
| StartLister(); |
| } else if (*enable_privet_notification_member_) { |
| ReportPrivetUmaEvent(PRIVET_SERVICE_STARTED); |
| traffic_detector_ = |
| new PrivetTrafficDetector( |
| net::ADDRESS_FAMILY_IPV4, |
| base::Bind(&PrivetNotificationService::StartLister, AsWeakPtr())); |
| traffic_detector_->Start(); |
| } else { |
| traffic_detector_ = nullptr; |
| device_lister_.reset(); |
| service_discovery_client_ = nullptr; |
| privet_notifications_listener_.reset(); |
| } |
| #else |
| if (IsForced() || *enable_privet_notification_member_) { |
| StartLister(); |
| } else { |
| device_lister_.reset(); |
| service_discovery_client_ = nullptr; |
| privet_notifications_listener_.reset(); |
| } |
| #endif |
| } |
| |
| void PrivetNotificationService::StartLister() { |
| ReportPrivetUmaEvent(PRIVET_LISTER_STARTED); |
| #if BUILDFLAG(ENABLE_MDNS) |
| traffic_detector_ = nullptr; |
| #endif // ENABLE_MDNS |
| service_discovery_client_ = |
| local_discovery::ServiceDiscoverySharedClient::GetInstance(); |
| device_lister_.reset( |
| new PrivetDeviceListerImpl(service_discovery_client_.get(), this)); |
| device_lister_->Start(); |
| device_lister_->DiscoverNewDevices(); |
| |
| std::unique_ptr<PrivetHTTPAsynchronousFactory> http_factory( |
| PrivetHTTPAsynchronousFactory::CreateInstance( |
| content::BrowserContext::GetDefaultStoragePartition(profile_)-> |
| GetURLRequestContext())); |
| |
| privet_notifications_listener_.reset( |
| new PrivetNotificationsListener(std::move(http_factory), this)); |
| } |
| |
| PrivetNotificationDelegate* |
| PrivetNotificationService::CreateNotificationDelegate(Profile* profile) { |
| return new PrivetNotificationDelegate(profile); |
| } |
| |
| PrivetNotificationDelegate::PrivetNotificationDelegate(Profile* profile) |
| : profile_(profile) {} |
| |
| PrivetNotificationDelegate::~PrivetNotificationDelegate() { |
| } |
| |
| void PrivetNotificationDelegate::Click( |
| const base::Optional<int>& button_index, |
| const base::Optional<base::string16>& reply) { |
| if (!button_index) |
| return; |
| |
| if (*button_index == 0) { |
| ReportPrivetUmaEvent(PRIVET_NOTIFICATION_CLICKED); |
| OpenTab(GURL(kPrivetNotificationOriginUrl)); |
| } else { |
| DCHECK_EQ(1, *button_index); |
| ReportPrivetUmaEvent(PRIVET_DISABLE_NOTIFICATIONS_CLICKED); |
| DisableNotifications(); |
| } |
| CloseNotification(); |
| } |
| |
| void PrivetNotificationDelegate::OpenTab(const GURL& url) { |
| NavigateParams params(profile_, url, ui::PAGE_TRANSITION_AUTO_TOPLEVEL); |
| params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| Navigate(¶ms); |
| } |
| |
| void PrivetNotificationDelegate::DisableNotifications() { |
| profile_->GetPrefs()->SetBoolean(prefs::kLocalDiscoveryNotificationsEnabled, |
| false); |
| } |
| |
| void PrivetNotificationDelegate::CloseNotification() { |
| NotificationDisplayService::GetForProfile(profile_)->Close( |
| NotificationHandler::Type::TRANSIENT, kPrivetNotificationID); |
| } |
| |
| } // namespace cloud_print |