blob: e627337a43a4d4f4e597a787e5203ee1e8b814c6 [file] [log] [blame]
// Copyright 2014 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/platform_notification_service_impl.h"
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/notifications/metrics/notification_metrics_logger.h"
#include "chrome/browser/notifications/metrics/notification_metrics_logger_factory.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/buildflags/buildflags.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/notifications/notification_resources.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "ui/message_center/public/cpp/features.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/public/cpp/notifier_id.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#endif
using content::BrowserContext;
using content::BrowserThread;
using content::NotificationDatabaseData;
using message_center::NotifierId;
namespace {
// Whether a web notification should be displayed when chrome is in full
// screen mode.
static bool ShouldDisplayWebNotificationOnFullScreen(Profile* profile,
const GURL& origin) {
#if defined(OS_ANDROID)
NOTIMPLEMENTED();
return false;
#endif // defined(OS_ANDROID)
// Check to see if this notification comes from a webpage that is displaying
// fullscreen content.
for (auto* browser : *BrowserList::GetInstance()) {
// Only consider the browsers for the profile that created the notification
if (browser->profile() != profile)
continue;
const content::WebContents* active_contents =
browser->tab_strip_model()->GetActiveWebContents();
if (!active_contents)
continue;
// Check to see if
// (a) the active tab in the browser shares its origin with the
// notification.
// (b) the browser is fullscreen
// (c) the browser has focus.
if (active_contents->GetURL().GetOrigin() == origin &&
browser->exclusive_access_manager()->context()->IsFullscreen() &&
browser->window()->IsActive()) {
return true;
}
}
return false;
}
} // namespace
// static
PlatformNotificationServiceImpl*
PlatformNotificationServiceImpl::GetInstance() {
return base::Singleton<PlatformNotificationServiceImpl>::get();
}
// static
void PlatformNotificationServiceImpl::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
// The first persistent ID is registered as 10000 rather than 1 to prevent the
// reuse of persistent notification IDs, which must be unique. Reuse of
// notification IDs may occur as they were previously stored in a different
// data store.
registry->RegisterIntegerPref(prefs::kNotificationNextPersistentId, 10000);
}
PlatformNotificationServiceImpl::PlatformNotificationServiceImpl() = default;
PlatformNotificationServiceImpl::~PlatformNotificationServiceImpl() = default;
bool PlatformNotificationServiceImpl::WasClosedProgrammatically(
const std::string& notification_id) {
return closed_notifications_.erase(notification_id) != 0;
}
// TODO(awdf): Rename to DisplayNonPersistentNotification (Similar for Close)
void PlatformNotificationServiceImpl::DisplayNotification(
BrowserContext* browser_context,
const std::string& notification_id,
const GURL& origin,
const blink::PlatformNotificationData& notification_data,
const blink::NotificationResources& notification_resources) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Posted tasks can request notifications to be added, which would cause a
// crash (see |ScopedKeepAlive|). We just do nothing here, the user would not
// see the notification anyway, since we are shutting down.
if (g_browser_process->IsShuttingDown())
return;
Profile* profile = Profile::FromBrowserContext(browser_context);
DCHECK(profile);
DCHECK_EQ(0u, notification_data.actions.size());
DCHECK_EQ(0u, notification_resources.action_icons.size());
message_center::Notification notification =
CreateNotificationFromData(profile, origin, notification_id,
notification_data, notification_resources);
NotificationDisplayServiceFactory::GetForProfile(profile)->Display(
NotificationHandler::Type::WEB_NON_PERSISTENT, notification);
}
void PlatformNotificationServiceImpl::DisplayPersistentNotification(
BrowserContext* browser_context,
const std::string& notification_id,
const GURL& service_worker_scope,
const GURL& origin,
const blink::PlatformNotificationData& notification_data,
const blink::NotificationResources& notification_resources) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Posted tasks can request notifications to be added, which would cause a
// crash (see |ScopedKeepAlive|). We just do nothing here, the user would not
// see the notification anyway, since we are shutting down.
if (g_browser_process->IsShuttingDown())
return;
Profile* profile = Profile::FromBrowserContext(browser_context);
DCHECK(profile);
message_center::Notification notification =
CreateNotificationFromData(profile, origin, notification_id,
notification_data, notification_resources);
auto metadata = std::make_unique<PersistentNotificationMetadata>();
metadata->service_worker_scope = service_worker_scope;
NotificationDisplayServiceFactory::GetForProfile(profile)->Display(
NotificationHandler::Type::WEB_PERSISTENT, notification,
std::move(metadata));
NotificationMetricsLoggerFactory::GetForBrowserContext(browser_context)
->LogPersistentNotificationShown();
}
void PlatformNotificationServiceImpl::CloseNotification(
BrowserContext* browser_context,
const std::string& notification_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
Profile* profile = Profile::FromBrowserContext(browser_context);
DCHECK(profile);
NotificationDisplayServiceFactory::GetForProfile(profile)->Close(
NotificationHandler::Type::WEB_NON_PERSISTENT, notification_id);
}
void PlatformNotificationServiceImpl::ClosePersistentNotification(
BrowserContext* browser_context,
const std::string& notification_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
Profile* profile = Profile::FromBrowserContext(browser_context);
DCHECK(profile);
closed_notifications_.insert(notification_id);
NotificationDisplayServiceFactory::GetForProfile(profile)->Close(
NotificationHandler::Type::WEB_PERSISTENT, notification_id);
}
void PlatformNotificationServiceImpl::GetDisplayedNotifications(
BrowserContext* browser_context,
const DisplayedNotificationsCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
Profile* profile = Profile::FromBrowserContext(browser_context);
// Tests will not have a message center.
if (!profile || profile->AsTestingProfile()) {
auto displayed_notifications = std::make_unique<std::set<std::string>>();
callback.Run(std::move(displayed_notifications),
false /* supports_synchronization */);
return;
}
NotificationDisplayServiceFactory::GetForProfile(profile)->GetDisplayed(
callback);
}
int64_t PlatformNotificationServiceImpl::ReadNextPersistentNotificationId(
BrowserContext* browser_context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PrefService* prefs = Profile::FromBrowserContext(browser_context)->GetPrefs();
int64_t current_id = prefs->GetInteger(prefs::kNotificationNextPersistentId);
int64_t next_id = current_id + 1;
prefs->SetInteger(prefs::kNotificationNextPersistentId, next_id);
return next_id;
}
void PlatformNotificationServiceImpl::RecordNotificationUkmEvent(
BrowserContext* browser_context,
const NotificationDatabaseData& data) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Only record the event if a user explicitly interacted with the notification
// to close it.
if (data.closed_reason != NotificationDatabaseData::ClosedReason::USER &&
data.num_clicks == 0 && data.num_action_button_clicks == 0) {
return;
}
// Query the HistoryService so we only record a notification if the origin is
// in the user's history.
Profile* profile = Profile::FromBrowserContext(browser_context);
history::HistoryService* history_service =
HistoryServiceFactory::GetForProfile(profile,
ServiceAccessType::EXPLICIT_ACCESS);
DCHECK(history_service);
history_service->QueryURL(
data.origin, /*want_visits=*/false,
base::BindOnce(
&PlatformNotificationServiceImpl::OnUrlHistoryQueryComplete,
base::Unretained(this), data),
&task_tracker_);
}
void PlatformNotificationServiceImpl::OnUrlHistoryQueryComplete(
const content::NotificationDatabaseData& data,
bool found_url,
const history::URLRow& url_row,
const history::VisitVector& visits) {
// Post the |history_query_complete_closure_for_testing_| closure to the
// current task runner to inform tests that the history query has completed.
if (history_query_complete_closure_for_testing_) {
base::PostTask(FROM_HERE,
std::move(history_query_complete_closure_for_testing_));
}
// Only record the notification if the |data.origin| is in the history
// service.
if (!found_url)
return;
ukm::UkmRecorder* recorder = ukm::UkmRecorder::Get();
DCHECK(recorder);
// We are using UpdateSourceURL as notifications are not tied to a navigation.
// This is ok from a privacy perspective as we have explicitly verified that
// |data.origin| is in the HistoryService before recording to UKM.
ukm::SourceId source_id = ukm::UkmRecorder::GetNewSourceID();
recorder->UpdateSourceURL(source_id, data.origin);
ukm::builders::Notification builder(source_id);
int64_t time_until_first_click_millis =
data.time_until_first_click_millis.has_value()
? data.time_until_first_click_millis.value().InMilliseconds()
: -1;
int64_t time_until_last_click_millis =
data.time_until_last_click_millis.has_value()
? data.time_until_last_click_millis.value().InMilliseconds()
: -1;
int64_t time_until_close_millis =
data.time_until_close_millis.has_value()
? data.time_until_close_millis.value().InMilliseconds()
: -1;
// TODO(yangsharon):Add did_user_open_settings field and update here.
builder.SetClosedReason(static_cast<int>(data.closed_reason))
.SetDidReplaceAnotherNotification(data.replaced_existing_notification)
.SetHasBadge(!data.notification_data.badge.is_empty())
.SetHasIcon(!data.notification_data.icon.is_empty())
.SetHasImage(!data.notification_data.image.is_empty())
.SetHasRenotify(data.notification_data.renotify)
.SetHasTag(!data.notification_data.tag.empty())
.SetIsSilent(data.notification_data.silent)
.SetNumActions(data.notification_data.actions.size())
.SetNumActionButtonClicks(data.num_action_button_clicks)
.SetNumClicks(data.num_clicks)
.SetRequireInteraction(data.notification_data.require_interaction)
.SetTimeUntilClose(time_until_close_millis)
.SetTimeUntilFirstClick(time_until_first_click_millis)
.SetTimeUntilLastClick(time_until_last_click_millis)
.Record(recorder);
}
message_center::Notification
PlatformNotificationServiceImpl::CreateNotificationFromData(
Profile* profile,
const GURL& origin,
const std::string& notification_id,
const blink::PlatformNotificationData& notification_data,
const blink::NotificationResources& notification_resources) const {
DCHECK_EQ(notification_data.actions.size(),
notification_resources.action_icons.size());
message_center::RichNotificationData optional_fields;
optional_fields.settings_button_handler =
base::FeatureList::IsEnabled(message_center::kNewStyleNotifications)
? message_center::SettingsButtonHandler::INLINE
: message_center::SettingsButtonHandler::DELEGATE;
// TODO(peter): Handle different screen densities instead of always using the
// 1x bitmap - crbug.com/585815.
message_center::Notification notification(
message_center::NOTIFICATION_TYPE_SIMPLE, notification_id,
notification_data.title, notification_data.body,
gfx::Image::CreateFrom1xBitmap(notification_resources.notification_icon),
base::UTF8ToUTF16(origin.host()), origin,
message_center::NotifierId(origin), optional_fields,
nullptr /* delegate */);
notification.set_context_message(
DisplayNameForContextMessage(profile, origin));
notification.set_vibration_pattern(notification_data.vibration_pattern);
notification.set_timestamp(notification_data.timestamp);
notification.set_renotify(notification_data.renotify);
notification.set_silent(notification_data.silent);
if (ShouldDisplayWebNotificationOnFullScreen(profile, origin)) {
notification.set_fullscreen_visibility(
message_center::FullscreenVisibility::OVER_USER);
}
if (!notification_resources.image.drawsNothing()) {
notification.set_type(message_center::NOTIFICATION_TYPE_IMAGE);
notification.set_image(
gfx::Image::CreateFrom1xBitmap(notification_resources.image));
}
// Badges are only supported on Android, primarily because it's the only
// platform that makes good use of them in the status bar.
#if defined(OS_ANDROID)
// TODO(peter): Handle different screen densities instead of always using the
// 1x bitmap - crbug.com/585815.
notification.set_small_image(
gfx::Image::CreateFrom1xBitmap(notification_resources.badge));
#endif // defined(OS_ANDROID)
// Developer supplied action buttons.
std::vector<message_center::ButtonInfo> buttons;
for (size_t i = 0; i < notification_data.actions.size(); ++i) {
const blink::PlatformNotificationAction& action =
notification_data.actions[i];
message_center::ButtonInfo button(action.title);
// TODO(peter): Handle different screen densities instead of always using
// the 1x bitmap - crbug.com/585815.
button.icon =
gfx::Image::CreateFrom1xBitmap(notification_resources.action_icons[i]);
if (action.type == blink::PLATFORM_NOTIFICATION_ACTION_TYPE_TEXT) {
button.placeholder = action.placeholder.as_optional_string16().value_or(
l10n_util::GetStringUTF16(IDS_NOTIFICATION_REPLY_PLACEHOLDER));
}
buttons.push_back(button);
}
notification.set_buttons(buttons);
// On desktop, notifications with require_interaction==true stay on-screen
// rather than minimizing to the notification center after a timeout.
// On mobile, this is ignored (notifications are minimized at all times).
if (notification_data.require_interaction)
notification.set_never_timeout(true);
return notification;
}
base::string16 PlatformNotificationServiceImpl::DisplayNameForContextMessage(
Profile* profile,
const GURL& origin) const {
#if BUILDFLAG(ENABLE_EXTENSIONS)
// If the source is an extension, lookup the display name.
if (origin.SchemeIs(extensions::kExtensionScheme)) {
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(profile)->GetExtensionById(
origin.host(), extensions::ExtensionRegistry::EVERYTHING);
DCHECK(extension);
return base::UTF8ToUTF16(extension->name());
}
#endif
return base::string16();
}