| // Copyright 2015 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 "content/browser/notifications/platform_notification_context_impl.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/task/post_task.h" |
| #include "content/browser/notifications/blink_notification_service_impl.h" |
| #include "content/browser/notifications/notification_database.h" |
| #include "content/browser/notifications/notification_trigger_constants.h" |
| #include "content/browser/notifications/platform_notification_service_proxy.h" |
| #include "content/browser/service_worker/service_worker_context_wrapper.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/notification_database_data.h" |
| #include "content/public/browser/permission_controller.h" |
| #include "content/public/browser/permission_type.h" |
| #include "content/public/browser/platform_notification_service.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "third_party/blink/public/common/notifications/notification_resources.h" |
| |
| namespace content { |
| namespace { |
| |
| // Name of the directory in the user's profile directory where the notification |
| // database files should be stored. |
| const base::FilePath::CharType kPlatformNotificationsDirectory[] = |
| FILE_PATH_LITERAL("Platform Notifications"); |
| |
| // Checks if this notification can trigger in the future. |
| bool CanTrigger(const NotificationDatabaseData& data) { |
| if (!base::FeatureList::IsEnabled(features::kNotificationTriggers)) |
| return false; |
| return data.notification_data.show_trigger_timestamp && !data.has_triggered; |
| } |
| |
| void LogNotificationTriggerUMA(const NotificationDatabaseData& data) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "Notifications.Triggers.HasShowTrigger", |
| data.notification_data.show_trigger_timestamp.has_value()); |
| |
| if (!data.notification_data.show_trigger_timestamp) |
| return; |
| |
| base::TimeDelta show_trigger_delay = |
| data.notification_data.show_trigger_timestamp.value() - base::Time::Now(); |
| |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Notifications.Triggers.ShowTriggerDelay", |
| show_trigger_delay.InDays(), 1, 365, 50); |
| } |
| |
| } // namespace |
| |
| PlatformNotificationContextImpl::PlatformNotificationContextImpl( |
| const base::FilePath& path, |
| BrowserContext* browser_context, |
| const scoped_refptr<ServiceWorkerContextWrapper>& service_worker_context) |
| : path_(path), |
| browser_context_(browser_context), |
| service_worker_context_(service_worker_context), |
| has_shutdown_(false) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| } |
| |
| PlatformNotificationContextImpl::~PlatformNotificationContextImpl() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // If the database has been initialized, it must be deleted on the task runner |
| // thread as closing it may cause file I/O. |
| if (database_) { |
| DCHECK(task_runner_); |
| task_runner_->DeleteSoon(FROM_HERE, database_.release()); |
| } |
| } |
| |
| void PlatformNotificationContextImpl::Initialize() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| service_proxy_ = std::make_unique<PlatformNotificationServiceProxy>( |
| service_worker_context_, browser_context_); |
| |
| PlatformNotificationService* service = |
| GetContentClient()->browser()->GetPlatformNotificationService( |
| browser_context_); |
| if (!service) { |
| std::set<std::string> displayed_notifications; |
| DidGetNotifications(std::move(displayed_notifications), false); |
| return; |
| } |
| |
| ukm_callback_ = base::BindRepeating( |
| &PlatformNotificationServiceProxy::RecordNotificationUkmEvent, |
| service_proxy_->AsWeakPtr()); |
| |
| service->GetDisplayedNotifications( |
| base::BindOnce(&PlatformNotificationContextImpl::DidGetNotifications, |
| this)); |
| } |
| |
| void PlatformNotificationContextImpl::DidGetNotifications( |
| std::set<std::string> displayed_notifications, |
| bool supports_synchronization) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // Abort if the profile has been shut down already. This mainly happens in |
| // tests and very short lived sessions. |
| if (has_shutdown_) |
| return; |
| |
| // Check if there are pending notifications to display. |
| base::Time next_trigger = base::Time::Max(); |
| if (service_proxy_ && |
| base::FeatureList::IsEnabled(features::kNotificationTriggers)) { |
| next_trigger = service_proxy_->GetNextTrigger(); |
| } |
| |
| // Synchronize the notifications stored in the database with the set of |
| // displaying notifications in |displayed_notifications|. This is necessary |
| // because flakiness may cause a platform to inform Chrome of a notification |
| // that has since been closed, or because the platform does not support |
| // notifications that exceed the lifetime of the browser process. |
| if (supports_synchronization || next_trigger <= base::Time::Now()) { |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoSyncNotificationData, this, |
| supports_synchronization, std::move(displayed_notifications))); |
| } else if (service_proxy_ && next_trigger != base::Time::Max()) { |
| service_proxy_->ScheduleTrigger(next_trigger); |
| } |
| |
| // |service_worker_context_| may be NULL in tests. |
| if (service_worker_context_) |
| service_worker_context_->AddObserver(this); |
| } |
| |
| void PlatformNotificationContextImpl::DoSyncNotificationData( |
| bool supports_synchronization, |
| std::set<std::string> displayed_notifications, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) |
| return; |
| |
| // Reset |next_trigger_| to keep track of the next trigger timestamp. |
| next_trigger_ = base::nullopt; |
| |
| // Iterate over all notifications and delete all expired ones. |
| NotificationDatabase::Status status = |
| database_->ForEachNotificationData(base::BindRepeating( |
| &PlatformNotificationContextImpl::DoHandleSyncNotification, this, |
| supports_synchronization, displayed_notifications)); |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| // Schedule the next trigger timestamp. |
| if (next_trigger_ && service_proxy_) |
| service_proxy_->ScheduleTrigger(next_trigger_.value()); |
| } |
| |
| void PlatformNotificationContextImpl::DoHandleSyncNotification( |
| bool supports_synchronization, |
| const std::set<std::string>& displayed_notifications, |
| const NotificationDatabaseData& data) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| // Handle pending notifications. |
| if (CanTrigger(data)) { |
| base::Time timestamp = |
| data.notification_data.show_trigger_timestamp.value(); |
| // Check if we should display this notification. |
| if (timestamp <= base::Time::Now()) |
| DoTriggerNotification(data); |
| else if (!next_trigger_ || next_trigger_.value() > timestamp) |
| next_trigger_ = timestamp; |
| return; |
| } |
| |
| // Do not delete notifications if the platform does not support syncing them. |
| if (!supports_synchronization) |
| return; |
| |
| // Delete notifications that are not on screen anymore. |
| if (!displayed_notifications.count(data.notification_id)) |
| database_->DeleteNotificationData(data.notification_id, data.origin); |
| } |
| |
| void PlatformNotificationContextImpl::Shutdown() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| has_shutdown_ = true; |
| |
| if (service_proxy_) |
| service_proxy_->Shutdown(); |
| |
| services_.clear(); |
| |
| // |service_worker_context_| may be NULL in tests. |
| if (service_worker_context_) |
| service_worker_context_->RemoveObserver(this); |
| } |
| |
| void PlatformNotificationContextImpl::CreateService( |
| const url::Origin& origin, |
| mojo::PendingReceiver<blink::mojom::NotificationService> receiver) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| services_.push_back(std::make_unique<BlinkNotificationServiceImpl>( |
| this, browser_context_, service_worker_context_, origin, |
| std::move(receiver))); |
| } |
| |
| void PlatformNotificationContextImpl::RemoveService( |
| BlinkNotificationServiceImpl* service) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| base::EraseIf( |
| services_, |
| [service](const std::unique_ptr<BlinkNotificationServiceImpl>& ptr) { |
| return ptr.get() == service; |
| }); |
| } |
| |
| void PlatformNotificationContextImpl:: |
| DeleteAllNotificationDataForBlockedOrigins( |
| DeleteAllResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoReadAllNotificationOrigins, this, |
| base::BindOnce( |
| &PlatformNotificationContextImpl::CheckPermissionsAndDeleteBlocked, |
| this, std::move(callback)))); |
| } |
| |
| void PlatformNotificationContextImpl::DoReadAllNotificationOrigins( |
| ReadAllOriginsResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| std::set<GURL> origins; |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| std::move(origins))); |
| return; |
| } |
| |
| NotificationDatabase::Status status = |
| database_->ForEachNotificationData(base::BindRepeating( |
| [](std::set<GURL>* origins, const NotificationDatabaseData& data) { |
| origins->insert(data.origin); |
| }, |
| &origins)); |
| |
| bool success = status == NotificationDatabase::STATUS_OK; |
| if (!success) |
| origins.clear(); |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask( |
| FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), success, std::move(origins))); |
| } |
| |
| void PlatformNotificationContextImpl::CheckPermissionsAndDeleteBlocked( |
| DeleteAllResultCallback callback, |
| bool success, |
| std::set<GURL> origins) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // Make sure |browser_context_| is still valid before getting the controller. |
| if (!success || !service_proxy_ || has_shutdown_) { |
| std::move(callback).Run(/* success= */ false, /* deleted_count= */ 0); |
| return; |
| } |
| |
| content::PermissionController* controller = |
| BrowserContext::GetPermissionController(browser_context_); |
| if (!controller) { |
| std::move(callback).Run(/* success= */ false, /* deleted_count= */ 0); |
| return; |
| } |
| |
| // Erase all valid origins so we're left with invalid ones. |
| base::EraseIf(origins, [controller](const GURL& origin) { |
| auto permission = controller->GetPermissionStatus( |
| PermissionType::NOTIFICATIONS, origin, origin); |
| return permission == blink::mojom::PermissionStatus::GRANTED; |
| }); |
| |
| if (origins.empty()) { |
| std::move(callback).Run(/* success= */ true, /* deleted_count= */ 0); |
| return; |
| } |
| |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoDeleteAllNotificationDataForOrigins, |
| this, std::move(origins), std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl::DoDeleteAllNotificationDataForOrigins( |
| std::set<GURL> origins, |
| DeleteAllResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| /* deleted_count= */ 0)); |
| return; |
| } |
| |
| std::set<std::string> deleted_notification_ids; |
| NotificationDatabase::Status status = NotificationDatabase::STATUS_OK; |
| for (const auto& origin : origins) { |
| status = database_->DeleteAllNotificationDataForOrigin( |
| origin, /* tag= */ "", &deleted_notification_ids); |
| if (status != NotificationDatabase::STATUS_OK) |
| break; |
| } |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DeleteAllForOriginsResult", |
| status, NotificationDatabase::STATUS_COUNT); |
| |
| bool success = status == NotificationDatabase::STATUS_OK; |
| |
| // Blow away the database if deleting data failed due to corruption. Following |
| // the contract of the delete methods, consider this to be a success as the |
| // caller's goal has been achieved: the data is gone. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) { |
| DestroyDatabase(); |
| success = true; |
| } |
| |
| if (service_proxy_) { |
| for (const std::string& notification_id : deleted_notification_ids) |
| service_proxy_->CloseNotification(notification_id); |
| } |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), success, |
| deleted_notification_ids.size())); |
| } |
| |
| void PlatformNotificationContextImpl::ReadNotificationDataAndRecordInteraction( |
| const std::string& notification_id, |
| const GURL& origin, |
| const PlatformNotificationContext::Interaction interaction, |
| ReadResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoReadNotificationData, this, |
| notification_id, origin, interaction, std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl::DoReadNotificationData( |
| const std::string& notification_id, |
| const GURL& origin, |
| Interaction interaction, |
| ReadResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| NotificationDatabaseData())); |
| return; |
| } |
| |
| NotificationDatabaseData database_data; |
| NotificationDatabase::Status status = |
| database_->ReadNotificationDataAndRecordInteraction( |
| notification_id, origin, interaction, &database_data); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.ReadResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| if (status == NotificationDatabase::STATUS_OK) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ true, |
| database_data)); |
| return; |
| } |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| NotificationDatabaseData())); |
| } |
| |
| void PlatformNotificationContextImpl::TriggerNotifications() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| std::set<std::string> displayed_notifications; |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoSyncNotificationData, this, |
| /* supports_synchronization= */ false, |
| std::move(displayed_notifications))); |
| } |
| |
| void PlatformNotificationContextImpl::DoTriggerNotification( |
| const NotificationDatabaseData& database_data) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| // Bail out in case we can not display the notification after Shutdown. |
| if (!service_proxy_) |
| return; |
| |
| blink::NotificationResources resources; |
| NotificationDatabase::Status status = database_->ReadNotificationResources( |
| database_data.notification_id, database_data.origin, &resources); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Notifications.Database.ReadResourcesForTriggeredResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| if (status != NotificationDatabase::STATUS_OK) |
| resources = blink::NotificationResources(); |
| |
| // Create a copy of the |database_data| to store the |has_triggered| flag. |
| NotificationDatabaseData write_database_data = database_data; |
| write_database_data.has_triggered = true; |
| status = database_->WriteNotificationData(write_database_data.origin, |
| write_database_data); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.WriteTriggeredResult", |
| status, NotificationDatabase::STATUS_COUNT); |
| |
| if (status != NotificationDatabase::STATUS_OK) { |
| database_->DeleteNotificationData(write_database_data.notification_id, |
| write_database_data.origin); |
| return; |
| } |
| |
| base::Time timestamp = |
| database_data.notification_data.show_trigger_timestamp.value(); |
| UMA_HISTOGRAM_LONG_TIMES("Notifications.Triggers.DisplayDelay", |
| base::Time::Now() - timestamp); |
| |
| // Remove resources from DB as we don't need them anymore. |
| database_->DeleteNotificationResources(write_database_data.notification_id, |
| write_database_data.origin); |
| |
| write_database_data.notification_resources = std::move(resources); |
| service_proxy_->DisplayNotification(std::move(write_database_data), |
| base::DoNothing()); |
| } |
| |
| void PlatformNotificationContextImpl::WriteNotificationResources( |
| std::vector<NotificationResourceData> resource_data, |
| WriteResourcesResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (has_shutdown_ || !service_proxy_) |
| return; |
| |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoWriteNotificationResources, this, |
| std::move(resource_data), std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl::DoWriteNotificationResources( |
| std::vector<NotificationResourceData> resource_data, |
| WriteResourcesResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false)); |
| return; |
| } |
| |
| NotificationDatabase::Status status = NotificationDatabase::STATUS_OK; |
| for (auto& data : resource_data) { |
| NotificationDatabaseData notification_data; |
| status = database_->ReadNotificationData(data.notification_id, data.origin, |
| ¬ification_data); |
| // Ignore missing notifications. |
| if (status == NotificationDatabase::STATUS_ERROR_NOT_FOUND) { |
| status = NotificationDatabase::STATUS_OK; |
| continue; |
| } |
| if (status != NotificationDatabase::STATUS_OK) |
| break; |
| |
| // We do not support storing action icons again as they are not used on |
| // Android N+ and this will only be used for Q+. |
| DCHECK(data.resources.action_icons.empty()); |
| size_t action_item_count = |
| notification_data.notification_data.actions.size(); |
| data.resources.action_icons.resize(action_item_count); |
| |
| notification_data.notification_resources = std::move(data.resources); |
| status = database_->WriteNotificationData(data.origin, notification_data); |
| if (status != NotificationDatabase::STATUS_OK) |
| break; |
| } |
| |
| if (status == NotificationDatabase::STATUS_OK) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ true)); |
| return; |
| } |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false)); |
| } |
| |
| void PlatformNotificationContextImpl::ReDisplayNotifications( |
| std::vector<GURL> origins, |
| ReDisplayNotificationsResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (has_shutdown_ || !service_proxy_) |
| return; |
| |
| LazyInitialize( |
| base::BindOnce(&PlatformNotificationContextImpl::DoReDisplayNotifications, |
| this, std::move(origins), std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl::DoReDisplayNotifications( |
| std::vector<GURL> origins, |
| ReDisplayNotificationsResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| size_t display_count = 0; |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), display_count)); |
| return; |
| } |
| |
| NotificationDatabase::Status status = NotificationDatabase::STATUS_OK; |
| for (const auto& origin : origins) { |
| std::vector<NotificationDatabaseData> datas; |
| status = database_->ReadAllNotificationDataForOrigin(origin, &datas); |
| if (status != NotificationDatabase::STATUS_OK) |
| break; |
| |
| for (const auto& data : datas) { |
| if (CanTrigger(data)) |
| continue; |
| blink::NotificationResources resources; |
| status = database_->ReadNotificationResources(data.notification_id, |
| data.origin, &resources); |
| // Ignore notifications without resources as they might already be shown. |
| if (status == NotificationDatabase::STATUS_ERROR_NOT_FOUND) { |
| status = NotificationDatabase::STATUS_OK; |
| continue; |
| } |
| if (status != NotificationDatabase::STATUS_OK) |
| break; |
| |
| // Remove resources from DB as we don't need them anymore. |
| database_->DeleteNotificationResources(data.notification_id, data.origin); |
| |
| NotificationDatabaseData display_data = data; |
| display_data.notification_resources = std::move(resources); |
| service_proxy_->DisplayNotification(std::move(display_data), |
| base::DoNothing()); |
| ++display_count; |
| } |
| if (status != NotificationDatabase::STATUS_OK) |
| break; |
| } |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, |
| {BrowserThread::UI, base::TaskPriority::USER_VISIBLE}, |
| base::BindOnce(std::move(callback), display_count)); |
| } |
| |
| void PlatformNotificationContextImpl::ReadNotificationResources( |
| const std::string& notification_id, |
| const GURL& origin, |
| ReadResourcesResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoReadNotificationResources, this, |
| notification_id, origin, std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl::DoReadNotificationResources( |
| const std::string& notification_id, |
| const GURL& origin, |
| ReadResourcesResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| blink::NotificationResources())); |
| return; |
| } |
| |
| blink::NotificationResources notification_resources; |
| NotificationDatabase::Status status = database_->ReadNotificationResources( |
| notification_id, origin, ¬ification_resources); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.ReadResourcesResult", |
| status, NotificationDatabase::STATUS_COUNT); |
| |
| if (status == NotificationDatabase::STATUS_OK) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ true, |
| notification_resources)); |
| return; |
| } |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| blink::NotificationResources())); |
| } |
| |
| void PlatformNotificationContextImpl:: |
| SynchronizeDisplayedNotificationsForServiceWorkerRegistration( |
| base::Time start_time, |
| const GURL& origin, |
| int64_t service_worker_registration_id, |
| ReadAllResultCallback callback, |
| std::set<std::string> notification_ids, |
| bool supports_synchronization) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize( |
| base::BindOnce(&PlatformNotificationContextImpl:: |
| DoReadAllNotificationDataForServiceWorkerRegistration, |
| this, start_time, origin, service_worker_registration_id, |
| std::move(callback), std::move(notification_ids), |
| supports_synchronization)); |
| } |
| |
| void PlatformNotificationContextImpl:: |
| ReadAllNotificationDataForServiceWorkerRegistration( |
| const GURL& origin, |
| int64_t service_worker_registration_id, |
| ReadAllResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| PlatformNotificationService* service = |
| GetContentClient()->browser()->GetPlatformNotificationService( |
| browser_context_); |
| |
| if (!service) { |
| // Rely on the database only |
| std::set<std::string> notification_ids; |
| SynchronizeDisplayedNotificationsForServiceWorkerRegistration( |
| base::Time::Now(), origin, service_worker_registration_id, |
| std::move(callback), std::move(notification_ids), |
| /* supports_synchronization= */ false); |
| return; |
| } |
| |
| service->GetDisplayedNotifications(base::BindOnce( |
| &PlatformNotificationContextImpl:: |
| SynchronizeDisplayedNotificationsForServiceWorkerRegistration, |
| this, base::Time::Now(), origin, service_worker_registration_id, |
| std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl:: |
| DoReadAllNotificationDataForServiceWorkerRegistration( |
| base::Time start_time, |
| const GURL& origin, |
| int64_t service_worker_registration_id, |
| ReadAllResultCallback callback, |
| std::set<std::string> displayed_notifications, |
| bool supports_synchronization, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| std::vector<NotificationDatabaseData>())); |
| return; |
| } |
| |
| std::vector<NotificationDatabaseData> notification_datas; |
| |
| NotificationDatabase::Status status = |
| database_->ReadAllNotificationDataForServiceWorkerRegistration( |
| origin, service_worker_registration_id, ¬ification_datas); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.ReadForServiceWorkerResult", |
| status, NotificationDatabase::STATUS_COUNT); |
| |
| std::vector<std::string> obsolete_notifications; |
| |
| if (status == NotificationDatabase::STATUS_OK) { |
| if (supports_synchronization) { |
| for (auto it = notification_datas.begin(); |
| it != notification_datas.end();) { |
| // The database is only used for persistent notifications. |
| DCHECK(NotificationIdGenerator::IsPersistentNotification( |
| it->notification_id)); |
| if (displayed_notifications.count(it->notification_id) || |
| CanTrigger(*it) || it->creation_time_millis >= start_time) { |
| ++it; |
| } else { |
| obsolete_notifications.push_back(it->notification_id); |
| it = notification_datas.erase(it); |
| } |
| } |
| } |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ true, |
| notification_datas)); |
| |
| // Remove notifications that are not actually on display anymore. |
| for (const auto& it : obsolete_notifications) |
| database_->DeleteNotificationData(it, origin); |
| return; |
| } |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| std::vector<NotificationDatabaseData>())); |
| } |
| |
| void PlatformNotificationContextImpl::WriteNotificationData( |
| int64_t persistent_notification_id, |
| int64_t service_worker_registration_id, |
| const GURL& origin, |
| const NotificationDatabaseData& database_data, |
| WriteResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoWriteNotificationData, this, |
| service_worker_registration_id, persistent_notification_id, origin, |
| database_data, std::move(callback))); |
| } |
| |
| bool PlatformNotificationContextImpl::DoCheckNotificationTriggerQuota( |
| const GURL& origin) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| int notification_count = 0; |
| // Iterate over all notifications and count all scheduled notifications for |
| // |origin|. |
| NotificationDatabase::Status status = |
| database_->ForEachNotificationData(base::BindRepeating( |
| [](const GURL& expected_origin, int* count, |
| const NotificationDatabaseData& data) { |
| if (CanTrigger(data) && data.origin == expected_origin) |
| *count = *count + 1; |
| }, |
| origin, ¬ification_count)); |
| |
| // Blow away the database if reading data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| return notification_count < kMaximumScheduledNotificationsPerOrigin; |
| } |
| |
| void PlatformNotificationContextImpl::DoWriteNotificationData( |
| int64_t service_worker_registration_id, |
| int64_t persistent_notification_id, |
| const GURL& origin, |
| const NotificationDatabaseData& database_data, |
| WriteResultCallback callback, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(database_data.notification_id.empty()); |
| if (!initialized || !service_proxy_) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| /* notification_id= */ "")); |
| return; |
| } |
| |
| if (base::FeatureList::IsEnabled(features::kNotificationTriggers)) |
| LogNotificationTriggerUMA(database_data); |
| |
| bool replaces_existing = false; |
| std::string notification_id = |
| notification_id_generator_.GenerateForPersistentNotification( |
| origin, database_data.notification_data.tag, |
| persistent_notification_id); |
| |
| // Eagerly delete data for replaced notifications from the database. |
| if (!database_data.notification_data.tag.empty()) { |
| std::set<std::string> deleted_notification_ids; |
| NotificationDatabase::Status delete_status = |
| database_->DeleteAllNotificationDataForOrigin( |
| origin, database_data.notification_data.tag, |
| &deleted_notification_ids); |
| |
| replaces_existing = deleted_notification_ids.count(notification_id) != 0; |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DeleteBeforeWriteResult", |
| delete_status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| // Unless the database was corrupted following this change, there is no |
| // reason to bail out here in event of failure because the notification |
| // display logic will handle notification replacement for the user. |
| if (delete_status == NotificationDatabase::STATUS_ERROR_CORRUPTED) { |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| /* notification_id= */ "")); |
| return; |
| } |
| } |
| |
| // Create a copy of the |database_data| to store a generated notification ID. |
| NotificationDatabaseData write_database_data = database_data; |
| write_database_data.notification_id = notification_id; |
| write_database_data.origin = origin; |
| |
| if (CanTrigger(write_database_data) && |
| !DoCheckNotificationTriggerQuota(origin)) { |
| // TODO(knollr): Reply with a custom error so developers can handle this. |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| /* notification_id= */ "")); |
| return; |
| } |
| |
| // Only store resources for notifications that will be scheduled. |
| if (!CanTrigger(write_database_data)) |
| write_database_data.notification_resources = base::nullopt; |
| |
| NotificationDatabase::Status status = |
| database_->WriteNotificationData(origin, write_database_data); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.WriteResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| if (status == NotificationDatabase::STATUS_OK) { |
| if (CanTrigger(write_database_data)) { |
| if (replaces_existing) |
| service_proxy_->CloseNotification(notification_id); |
| |
| // Schedule notification to be shown. |
| service_proxy_->ScheduleNotification(std::move(write_database_data)); |
| |
| // Respond with success as this notification got scheduled successfully. |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ true, |
| notification_id)); |
| return; |
| } |
| |
| // Display the notification immediately. |
| write_database_data.notification_resources = |
| database_data.notification_resources; |
| service_proxy_->DisplayNotification(std::move(write_database_data), |
| std::move(callback)); |
| return; |
| } |
| |
| // Blow away the database if writing data failed due to corruption. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), /* success= */ false, |
| /* notification_id= */ "")); |
| } |
| |
| void PlatformNotificationContextImpl::DeleteNotificationData( |
| const std::string& notification_id, |
| const GURL& origin, |
| bool close_notification, |
| DeleteResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!service_proxy_) |
| return; |
| |
| // Close notification as we're about to delete its data. |
| if (close_notification) |
| service_proxy_->CloseNotification(notification_id); |
| |
| bool should_log_close = service_proxy_->ShouldLogClose(origin); |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::DoDeleteNotificationData, this, |
| notification_id, origin, std::move(callback), should_log_close)); |
| } |
| |
| void PlatformNotificationContextImpl::DoDeleteNotificationData( |
| const std::string& notification_id, |
| const GURL& origin, |
| DeleteResultCallback callback, |
| bool should_log_close, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) { |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), false)); |
| return; |
| } |
| |
| // Read additional data if we need to log the close event. |
| if (should_log_close) { |
| NotificationDatabaseData data; |
| if (database_->ReadNotificationData(notification_id, origin, &data) == |
| NotificationDatabase::STATUS_OK) { |
| service_proxy_->LogClose(std::move(data)); |
| } |
| } |
| |
| NotificationDatabase::Status status = |
| database_->DeleteNotificationData(notification_id, origin); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DeleteResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| bool success = status == NotificationDatabase::STATUS_OK; |
| |
| // Blow away the database if deleting data failed due to corruption. Following |
| // the contract of the delete methods, consider this to be a success as the |
| // caller's goal has been achieved: the data is gone. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) { |
| DestroyDatabase(); |
| success = true; |
| } |
| |
| base::PostTask(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(std::move(callback), success)); |
| } |
| |
| void PlatformNotificationContextImpl::OnRegistrationDeleted( |
| int64_t registration_id, |
| const GURL& pattern) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize( |
| base::BindOnce(&PlatformNotificationContextImpl:: |
| DoDeleteNotificationsForServiceWorkerRegistration, |
| this, pattern.GetOrigin(), registration_id)); |
| } |
| |
| void PlatformNotificationContextImpl:: |
| DoDeleteNotificationsForServiceWorkerRegistration( |
| const GURL& origin, |
| int64_t service_worker_registration_id, |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) |
| return; |
| |
| std::set<std::string> deleted_notification_ids; |
| NotificationDatabase::Status status = |
| database_->DeleteAllNotificationDataForServiceWorkerRegistration( |
| origin, service_worker_registration_id, &deleted_notification_ids); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Notifications.Database.DeleteServiceWorkerRegistrationResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| // Blow away the database if a corruption error occurred during the deletion. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) |
| DestroyDatabase(); |
| |
| if (service_proxy_) { |
| for (const std::string& notification_id : deleted_notification_ids) |
| service_proxy_->CloseNotification(notification_id); |
| } |
| } |
| |
| void PlatformNotificationContextImpl::OnStorageWiped() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| LazyInitialize(base::BindOnce( |
| &PlatformNotificationContextImpl::OnStorageWipedInitialized, this)); |
| } |
| |
| void PlatformNotificationContextImpl::OnStorageWipedInitialized( |
| bool initialized) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (!initialized) |
| return; |
| DestroyDatabase(); |
| } |
| |
| void PlatformNotificationContextImpl::LazyInitialize( |
| InitializeResultCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (!task_runner_) { |
| task_runner_ = |
| base::CreateSequencedTaskRunner({base::ThreadPool(), base::MayBlock(), |
| base::TaskPriority::USER_VISIBLE}); |
| } |
| |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&PlatformNotificationContextImpl::OpenDatabase, |
| this, std::move(callback))); |
| } |
| |
| void PlatformNotificationContextImpl::OpenDatabase( |
| InitializeResultCallback callback) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| if (database_) { |
| std::move(callback).Run(/* initialized= */ true); |
| return; |
| } |
| |
| database_.reset(new NotificationDatabase(GetDatabasePath(), ukm_callback_)); |
| NotificationDatabase::Status status = |
| database_->Open(/* create_if_missing= */ true); |
| |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.OpenResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| // When the database could not be opened due to corruption, destroy it, blow |
| // away the contents of the directory and try re-opening the database. |
| if (status == NotificationDatabase::STATUS_ERROR_CORRUPTED) { |
| if (DestroyDatabase()) { |
| database_.reset( |
| new NotificationDatabase(GetDatabasePath(), ukm_callback_)); |
| status = database_->Open(/* create_if_missing= */ true); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Notifications.Database.OpenAfterCorruptionResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| } |
| } |
| |
| if (status == NotificationDatabase::STATUS_OK) { |
| std::move(callback).Run(/* initialized= */ true); |
| return; |
| } |
| |
| database_.reset(); |
| |
| std::move(callback).Run(/* initialized= */ false); |
| } |
| |
| bool PlatformNotificationContextImpl::DestroyDatabase() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(database_); |
| |
| NotificationDatabase::Status status = database_->Destroy(); |
| UMA_HISTOGRAM_ENUMERATION("Notifications.Database.DestroyResult", status, |
| NotificationDatabase::STATUS_COUNT); |
| |
| database_.reset(); |
| |
| // TODO(peter): Close any existing persistent notifications on the platform. |
| |
| // Remove all files in the directory that the database was previously located |
| // in, to make sure that any left-over files are gone as well. |
| base::FilePath database_path = GetDatabasePath(); |
| return database_path.empty() || base::DeleteFileRecursively(database_path); |
| } |
| |
| base::FilePath PlatformNotificationContextImpl::GetDatabasePath() const { |
| if (path_.empty()) |
| return path_; |
| |
| return path_.Append(kPlatformNotificationsDirectory); |
| } |
| |
| void PlatformNotificationContextImpl::SetTaskRunnerForTesting( |
| const scoped_refptr<base::SequencedTaskRunner>& task_runner) { |
| task_runner_ = task_runner; |
| } |
| |
| } // namespace content |