| // Copyright 2019 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/devtools/devtools_background_services_context_impl.h" | 
 |  | 
 | #include <algorithm> | 
 |  | 
 | #include "base/guid.h" | 
 | #include "base/metrics/histogram_macros.h" | 
 | #include "base/observer_list.h" | 
 | #include "base/strings/string_number_conversions.h" | 
 | #include "base/time/time.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/service_worker_context.h" | 
 | #include "content/public/common/content_client.h" | 
 | #include "third_party/blink/public/common/storage_key/storage_key.h" | 
 | #include "url/origin.h" | 
 |  | 
 | namespace content { | 
 |  | 
 | namespace { | 
 |  | 
 | std::string CreateEntryKeyPrefix(devtools::proto::BackgroundService service) { | 
 |   DCHECK_NE(service, devtools::proto::BackgroundService::UNKNOWN); | 
 |   return "devtools_background_services_" + base::NumberToString(service) + "_"; | 
 | } | 
 |  | 
 | std::string CreateEntryKey(devtools::proto::BackgroundService service) { | 
 |   return CreateEntryKeyPrefix(service) + base::GenerateGUID(); | 
 | } | 
 |  | 
 | void DidLogServiceEvent(blink::ServiceWorkerStatusCode status) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |   UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.LogEvent", status); | 
 | } | 
 |  | 
 | void DidClearServiceEvents(blink::ServiceWorkerStatusCode status) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |   UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.ClearEvents", status); | 
 | } | 
 |  | 
 | constexpr devtools::proto::BackgroundService ServiceToProtoEnum( | 
 |     DevToolsBackgroundService service) { | 
 |   switch (service) { | 
 |     case DevToolsBackgroundService::kBackgroundFetch: | 
 |       return devtools::proto::BACKGROUND_FETCH; | 
 |     case DevToolsBackgroundService::kBackgroundSync: | 
 |       return devtools::proto::BACKGROUND_SYNC; | 
 |     case DevToolsBackgroundService::kPushMessaging: | 
 |       return devtools::proto::PUSH_MESSAGING; | 
 |     case DevToolsBackgroundService::kNotifications: | 
 |       return devtools::proto::NOTIFICATIONS; | 
 |     case DevToolsBackgroundService::kPaymentHandler: | 
 |       return devtools::proto::PAYMENT_HANDLER; | 
 |     case DevToolsBackgroundService::kPeriodicBackgroundSync: | 
 |       return devtools::proto::PERIODIC_BACKGROUND_SYNC; | 
 |   } | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | DevToolsBackgroundServicesContextImpl::DevToolsBackgroundServicesContextImpl( | 
 |     BrowserContext* browser_context, | 
 |     scoped_refptr<ServiceWorkerContextWrapper> service_worker_context) | 
 |     : browser_context_(browser_context), | 
 |       service_worker_context_(std::move(service_worker_context)) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   auto expiration_times = | 
 |       GetContentClient()->browser()->GetDevToolsBackgroundServiceExpirations( | 
 |           browser_context_); | 
 |  | 
 |   for (const auto& expiration_time : expiration_times) { | 
 |     DCHECK(devtools::proto::BackgroundService_IsValid(expiration_time.first)); | 
 |     expiration_times_[expiration_time.first] = expiration_time.second; | 
 |  | 
 |     auto service = | 
 |         static_cast<devtools::proto::BackgroundService>(expiration_time.first); | 
 |     // If the recording permission for |service| has expired, set it to null. | 
 |     if (IsRecordingExpired(service)) | 
 |       expiration_times_[expiration_time.first] = base::Time(); | 
 |   } | 
 | } | 
 |  | 
 | DevToolsBackgroundServicesContextImpl:: | 
 |     ~DevToolsBackgroundServicesContextImpl() = default; | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::AddObserver( | 
 |     EventObserver* observer) { | 
 |   observers_.AddObserver(observer); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::RemoveObserver( | 
 |     const EventObserver* observer) { | 
 |   observers_.RemoveObserver(observer); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::StartRecording( | 
 |     devtools::proto::BackgroundService service) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   // TODO(rayankans): Make the time delay finch configurable. | 
 |   base::Time expiration_time = base::Time::Now() + base::Days(3); | 
 |   expiration_times_[service] = expiration_time; | 
 |  | 
 |   GetContentClient()->browser()->UpdateDevToolsBackgroundServiceExpiration( | 
 |       browser_context_, service, expiration_time); | 
 |  | 
 |   for (EventObserver& observer : observers_) | 
 |     observer.OnRecordingStateChanged(/* should_record= */ true, service); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::StopRecording( | 
 |     devtools::proto::BackgroundService service) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   expiration_times_[service] = base::Time(); | 
 |   GetContentClient()->browser()->UpdateDevToolsBackgroundServiceExpiration( | 
 |       browser_context_, service, base::Time()); | 
 |  | 
 |   for (EventObserver& observer : observers_) | 
 |     observer.OnRecordingStateChanged(/* should_record= */ false, service); | 
 | } | 
 |  | 
 | bool DevToolsBackgroundServicesContextImpl::IsRecording( | 
 |     DevToolsBackgroundService service) { | 
 |   return IsRecording(ServiceToProtoEnum(service)); | 
 | } | 
 |  | 
 | bool DevToolsBackgroundServicesContextImpl::IsRecording( | 
 |     devtools::proto::BackgroundService service) { | 
 |   // Returns whether |service| has been enabled. When the expiration time has | 
 |   // been met it will be lazily updated to be null. | 
 |   return !expiration_times_[service].is_null(); | 
 | } | 
 |  | 
 | bool DevToolsBackgroundServicesContextImpl::IsRecordingExpired( | 
 |     devtools::proto::BackgroundService service) { | 
 |   // Copy the expiration time to avoid data races. | 
 |   const base::Time expiration_time = expiration_times_[service]; | 
 |   return !expiration_time.is_null() && expiration_time < base::Time::Now(); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::GetLoggedBackgroundServiceEvents( | 
 |     devtools::proto::BackgroundService service, | 
 |     GetLoggedBackgroundServiceEventsCallback callback) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   service_worker_context_->GetUserDataForAllRegistrationsByKeyPrefix( | 
 |       CreateEntryKeyPrefix(service), | 
 |       base::BindOnce(&DevToolsBackgroundServicesContextImpl::DidGetUserData, | 
 |                      weak_ptr_factory_.GetWeakPtr(), std::move(callback))); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::DidGetUserData( | 
 |     GetLoggedBackgroundServiceEventsCallback callback, | 
 |     const std::vector<std::pair<int64_t, std::string>>& user_data, | 
 |     blink::ServiceWorkerStatusCode status) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.GetEvents", status); | 
 |  | 
 |   std::vector<devtools::proto::BackgroundServiceEvent> events; | 
 |  | 
 |   if (status != blink::ServiceWorkerStatusCode::kOk) { | 
 |     std::move(callback).Run(events); | 
 |     return; | 
 |   } | 
 |  | 
 |   events.reserve(user_data.size()); | 
 |   for (const auto& data : user_data) { | 
 |     devtools::proto::BackgroundServiceEvent event; | 
 |     if (!event.ParseFromString(data.second)) { | 
 |       // TODO(rayankans): Log errors to UMA. | 
 |       std::move(callback).Run({}); | 
 |       return; | 
 |     } | 
 |     DCHECK_EQ(data.first, event.service_worker_registration_id()); | 
 |     events.push_back(std::move(event)); | 
 |   } | 
 |  | 
 |   std::sort(events.begin(), events.end(), | 
 |             [](const auto& state1, const auto& state2) { | 
 |               return state1.timestamp() < state2.timestamp(); | 
 |             }); | 
 |  | 
 |   GetUIThreadTaskRunner({})->PostTask( | 
 |       FROM_HERE, base::BindOnce(std::move(callback), std::move(events))); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::ClearLoggedBackgroundServiceEvents( | 
 |     devtools::proto::BackgroundService service) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   service_worker_context_->ClearUserDataForAllRegistrationsByKeyPrefix( | 
 |       CreateEntryKeyPrefix(service), base::BindOnce(&DidClearServiceEvents)); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::LogBackgroundServiceEvent( | 
 |     uint64_t service_worker_registration_id, | 
 |     const url::Origin& origin, | 
 |     DevToolsBackgroundService service, | 
 |     const std::string& event_name, | 
 |     const std::string& instance_id, | 
 |     const std::map<std::string, std::string>& event_metadata) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   if (!IsRecording(service)) | 
 |     return; | 
 |  | 
 |   if (IsRecordingExpired(ServiceToProtoEnum(service))) { | 
 |     // We should stop recording because of the expiration time. We should | 
 |     // also inform the observers that we stopped recording. | 
 |     OnRecordingTimeExpired(ServiceToProtoEnum(service)); | 
 |     return; | 
 |   } | 
 |  | 
 |   devtools::proto::BackgroundServiceEvent event; | 
 |   event.set_timestamp( | 
 |       base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds()); | 
 |   event.set_origin(origin.GetURL().spec()); | 
 |   event.set_service_worker_registration_id(service_worker_registration_id); | 
 |   event.set_background_service(ServiceToProtoEnum(service)); | 
 |   event.set_event_name(event_name); | 
 |   event.set_instance_id(instance_id); | 
 |   event.mutable_event_metadata()->insert(event_metadata.begin(), | 
 |                                          event_metadata.end()); | 
 |  | 
 |   // TODO(crbug.com/1199077): Update this when | 
 |   // DevToolsBackgroundServicesContextImpl implements StorageKey. | 
 |   service_worker_context_->StoreRegistrationUserData( | 
 |       service_worker_registration_id, blink::StorageKey(origin), | 
 |       {{CreateEntryKey(event.background_service()), event.SerializeAsString()}}, | 
 |       base::BindOnce(&DidLogServiceEvent)); | 
 |  | 
 |   NotifyEventObservers(std::move(event)); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::NotifyEventObservers( | 
 |     const devtools::proto::BackgroundServiceEvent& event) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   for (EventObserver& observer : observers_) | 
 |     observer.OnEventReceived(event); | 
 | } | 
 |  | 
 | void DevToolsBackgroundServicesContextImpl::OnRecordingTimeExpired( | 
 |     devtools::proto::BackgroundService service) { | 
 |   DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
 |  | 
 |   // This could have been stopped by the user in the meanwhile, or we | 
 |   // received duplicate time expiry events. | 
 |   if (IsRecordingExpired(service)) | 
 |     StopRecording(service); | 
 | } | 
 |  | 
 | }  // namespace content |