browsing_topics_service_impl.cc 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. // Copyright 2022 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/browsing_topics/browsing_topics_service_impl.h"
  5. #include <random>
  6. #include "base/rand_util.h"
  7. #include "base/ranges/algorithm.h"
  8. #include "base/time/time.h"
  9. #include "components/browsing_topics/browsing_topics_calculator.h"
  10. #include "components/browsing_topics/browsing_topics_page_load_data_tracker.h"
  11. #include "components/browsing_topics/mojom/browsing_topics_internals.mojom.h"
  12. #include "components/browsing_topics/util.h"
  13. #include "components/optimization_guide/content/browser/page_content_annotations_service.h"
  14. #include "content/public/browser/browsing_topics_site_data_manager.h"
  15. #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
  16. #include "services/metrics/public/cpp/ukm_builders.h"
  17. #include "services/metrics/public/cpp/ukm_recorder.h"
  18. #include "third_party/blink/public/common/features.h"
  19. #include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h"
  20. namespace browsing_topics {
  21. namespace {
  22. // Returns whether the topics should all be cleared given
  23. // `browsing_topics_data_accessible_since` and `is_topic_allowed_by_settings`.
  24. // Returns true if `browsing_topics_data_accessible_since` is greater than the
  25. // last calculation time, or if any top topic is disallowed from the settings.
  26. // The latter could happen if the topic became disallowed when
  27. // `browsing_topics_state` was still loading (and we didn't get a chance to
  28. // clear it). This is an unlikely edge case, so it's fine to over-delete.
  29. bool ShouldClearTopicsOnStartup(
  30. const BrowsingTopicsState& browsing_topics_state,
  31. base::Time browsing_topics_data_accessible_since,
  32. base::RepeatingCallback<bool(const privacy_sandbox::CanonicalTopic&)>
  33. is_topic_allowed_by_settings) {
  34. DCHECK(!is_topic_allowed_by_settings.is_null());
  35. if (browsing_topics_state.epochs().empty())
  36. return false;
  37. // Here we rely on the fact that `browsing_topics_data_accessible_since` can
  38. // only be updated to base::Time::Now() due to data deletion. So we'll either
  39. // need to clear all topics data, or no-op. If this assumption no longer
  40. // holds, we'd need to iterate over all epochs, check their calculation time,
  41. // and selectively delete the epochs.
  42. if (browsing_topics_data_accessible_since >
  43. browsing_topics_state.epochs().back().calculation_time()) {
  44. return true;
  45. }
  46. for (const EpochTopics& epoch : browsing_topics_state.epochs()) {
  47. for (const TopicAndDomains& topic_and_domains :
  48. epoch.top_topics_and_observing_domains()) {
  49. if (!topic_and_domains.IsValid())
  50. continue;
  51. if (!is_topic_allowed_by_settings.Run(privacy_sandbox::CanonicalTopic(
  52. topic_and_domains.topic(), epoch.taxonomy_version()))) {
  53. return true;
  54. }
  55. }
  56. }
  57. return false;
  58. }
  59. struct StartupCalculateDecision {
  60. bool clear_topics_data = true;
  61. base::TimeDelta next_calculation_delay;
  62. };
  63. StartupCalculateDecision GetStartupCalculationDecision(
  64. const BrowsingTopicsState& browsing_topics_state,
  65. base::Time browsing_topics_data_accessible_since,
  66. base::RepeatingCallback<bool(const privacy_sandbox::CanonicalTopic&)>
  67. is_topic_allowed_by_settings) {
  68. // The topics have never been calculated. This could happen with a fresh
  69. // profile or the if the config has updated. In case of a config update, the
  70. // topics should have already been cleared when initializing the
  71. // `BrowsingTopicsState`.
  72. if (browsing_topics_state.next_scheduled_calculation_time().is_null()) {
  73. return StartupCalculateDecision{
  74. .clear_topics_data = false,
  75. .next_calculation_delay = base::TimeDelta()};
  76. }
  77. // This could happen when clear-on-exit is turned on and has caused the
  78. // cookies to be deleted on startup, of if a topic became disallowed when
  79. // `browsing_topics_state` was still loading.
  80. bool should_clear_topics_data = ShouldClearTopicsOnStartup(
  81. browsing_topics_state, browsing_topics_data_accessible_since,
  82. is_topic_allowed_by_settings);
  83. base::TimeDelta presumed_next_calculation_delay =
  84. browsing_topics_state.next_scheduled_calculation_time() -
  85. base::Time::Now();
  86. // The scheduled calculation time was reached before the startup.
  87. if (presumed_next_calculation_delay <= base::TimeDelta()) {
  88. return StartupCalculateDecision{
  89. .clear_topics_data = should_clear_topics_data,
  90. .next_calculation_delay = base::TimeDelta()};
  91. }
  92. // This could happen if the machine time has changed since the last
  93. // calculation. Recalculate immediately to align with the expected schedule
  94. // rather than potentially stop computing for a very long time.
  95. if (presumed_next_calculation_delay >=
  96. 2 * blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get()) {
  97. return StartupCalculateDecision{
  98. .clear_topics_data = should_clear_topics_data,
  99. .next_calculation_delay = base::TimeDelta()};
  100. }
  101. return StartupCalculateDecision{
  102. .clear_topics_data = should_clear_topics_data,
  103. .next_calculation_delay = presumed_next_calculation_delay};
  104. }
  105. // Represents the different reasons why the topics API returns an empty result.
  106. // These values are persisted to logs. Entries should not be renumbered and
  107. // numeric values should never be reused.
  108. enum class EmptyApiResultReason {
  109. // The topics state hasn't finished loading.
  110. kStateNotReady = 0,
  111. // Access is disallowed by user settings.
  112. kAccessDisallowedBySettings = 1,
  113. // There are no candidate topics, e.g. no candidate epochs; epoch calculation
  114. // failed; individual topics were cleared or blocked.
  115. kNoCandicateTopics = 2,
  116. // The candidate topics were filtered for the requesting context.
  117. kCandicateTopicsFiltered = 3,
  118. kMaxValue = kCandicateTopicsFiltered,
  119. };
  120. void RecordBrowsingTopicsApiResultUkmMetrics(
  121. EmptyApiResultReason empty_reason,
  122. content::RenderFrameHost* main_frame) {
  123. ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
  124. ukm::builders::BrowsingTopics_DocumentBrowsingTopicsApiResult builder(
  125. main_frame->GetPageUkmSourceId());
  126. builder.SetEmptyReason(static_cast<int64_t>(empty_reason));
  127. builder.Record(ukm_recorder->Get());
  128. }
  129. void RecordBrowsingTopicsApiResultUkmMetrics(
  130. const std::vector<std::pair<blink::mojom::EpochTopicPtr, bool>>&
  131. topics_with_status,
  132. content::RenderFrameHost* main_frame) {
  133. DCHECK(!topics_with_status.empty());
  134. ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
  135. ukm::builders::BrowsingTopics_DocumentBrowsingTopicsApiResult builder(
  136. main_frame->GetPageUkmSourceId());
  137. for (size_t i = 0; i < 3u && topics_with_status.size() > i; ++i) {
  138. const blink::mojom::EpochTopicPtr& topic = topics_with_status[i].first;
  139. bool is_true_topic = topics_with_status[i].second;
  140. int taxonomy_version = 0;
  141. base::StringToInt(topic->taxonomy_version, &taxonomy_version);
  142. DCHECK(taxonomy_version);
  143. int64_t model_version = 0;
  144. base::StringToInt64(topic->model_version, &model_version);
  145. DCHECK(model_version);
  146. if (i == 0) {
  147. builder.SetReturnedTopic0(topic->topic)
  148. .SetReturnedTopic0IsTrueTopTopic(is_true_topic)
  149. .SetReturnedTopic0TaxonomyVersion(taxonomy_version)
  150. .SetReturnedTopic0ModelVersion(model_version);
  151. } else if (i == 1) {
  152. builder.SetReturnedTopic1(topic->topic)
  153. .SetReturnedTopic1IsTrueTopTopic(is_true_topic)
  154. .SetReturnedTopic1TaxonomyVersion(taxonomy_version)
  155. .SetReturnedTopic1ModelVersion(model_version);
  156. } else {
  157. DCHECK_EQ(i, 2u);
  158. builder.SetReturnedTopic2(topic->topic)
  159. .SetReturnedTopic2IsTrueTopTopic(is_true_topic)
  160. .SetReturnedTopic2TaxonomyVersion(taxonomy_version)
  161. .SetReturnedTopic2ModelVersion(model_version);
  162. }
  163. }
  164. builder.Record(ukm_recorder->Get());
  165. }
  166. } // namespace
  167. BrowsingTopicsServiceImpl::~BrowsingTopicsServiceImpl() = default;
  168. BrowsingTopicsServiceImpl::BrowsingTopicsServiceImpl(
  169. const base::FilePath& profile_path,
  170. privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings,
  171. history::HistoryService* history_service,
  172. content::BrowsingTopicsSiteDataManager* site_data_manager,
  173. optimization_guide::PageContentAnnotationsService* annotations_service)
  174. : privacy_sandbox_settings_(privacy_sandbox_settings),
  175. history_service_(history_service),
  176. site_data_manager_(site_data_manager),
  177. annotations_service_(annotations_service),
  178. browsing_topics_state_(
  179. profile_path,
  180. base::BindOnce(
  181. &BrowsingTopicsServiceImpl::OnBrowsingTopicsStateLoaded,
  182. base::Unretained(this))) {
  183. privacy_sandbox_settings_observation_.Observe(privacy_sandbox_settings);
  184. history_service_observation_.Observe(history_service);
  185. // Greedily request the model to be available to reduce the latency in later
  186. // topics calculation.
  187. annotations_service_->RequestAndNotifyWhenModelAvailable(
  188. optimization_guide::AnnotationType::kPageTopics, base::DoNothing());
  189. }
  190. std::vector<blink::mojom::EpochTopicPtr>
  191. BrowsingTopicsServiceImpl::GetBrowsingTopicsForJsApi(
  192. const url::Origin& context_origin,
  193. content::RenderFrameHost* main_frame) {
  194. if (!browsing_topics_state_loaded_) {
  195. RecordBrowsingTopicsApiResultUkmMetrics(
  196. EmptyApiResultReason::kStateNotReady, main_frame);
  197. return {};
  198. }
  199. if (!privacy_sandbox_settings_->IsTopicsAllowed()) {
  200. RecordBrowsingTopicsApiResultUkmMetrics(
  201. EmptyApiResultReason::kAccessDisallowedBySettings, main_frame);
  202. return {};
  203. }
  204. if (!privacy_sandbox_settings_->IsTopicsAllowedForContext(
  205. context_origin.GetURL(), main_frame->GetLastCommittedOrigin())) {
  206. RecordBrowsingTopicsApiResultUkmMetrics(
  207. EmptyApiResultReason::kAccessDisallowedBySettings, main_frame);
  208. return {};
  209. }
  210. std::string context_domain =
  211. net::registry_controlled_domains::GetDomainAndRegistry(
  212. context_origin.GetURL(),
  213. net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  214. HashedDomain hashed_context_domain = HashContextDomainForStorage(
  215. browsing_topics_state_.hmac_key(), context_domain);
  216. // Track the API usage context after the permissions check.
  217. BrowsingTopicsPageLoadDataTracker::GetOrCreateForPage(main_frame->GetPage())
  218. ->OnBrowsingTopicsApiUsed(hashed_context_domain, history_service_);
  219. std::string top_domain =
  220. net::registry_controlled_domains::GetDomainAndRegistry(
  221. main_frame->GetLastCommittedOrigin().GetURL(),
  222. net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  223. bool has_filtered_topics = false;
  224. // The result topics along with flags denoting whether they are true topics.
  225. std::vector<std::pair<blink::mojom::EpochTopicPtr, bool>> topics_with_status;
  226. for (const EpochTopics* epoch :
  227. browsing_topics_state_.EpochsForSite(top_domain)) {
  228. bool output_is_true_topic = false;
  229. bool candidate_topic_filtered = false;
  230. absl::optional<Topic> topic = epoch->TopicForSite(
  231. top_domain, hashed_context_domain, browsing_topics_state_.hmac_key(),
  232. output_is_true_topic, candidate_topic_filtered);
  233. if (candidate_topic_filtered)
  234. has_filtered_topics = true;
  235. // Only add a non-empty topic to the result.
  236. if (!topic)
  237. continue;
  238. // Although a top topic can never be in the disallowed state, the returned
  239. // `topic` may be the random one. Thus we still need this check.
  240. if (!privacy_sandbox_settings_->IsTopicAllowed(
  241. privacy_sandbox::CanonicalTopic(*topic,
  242. epoch->taxonomy_version()))) {
  243. continue;
  244. }
  245. auto result_topic = blink::mojom::EpochTopic::New();
  246. result_topic->topic = topic.value().value();
  247. result_topic->config_version = base::StrCat(
  248. {"chrome.", base::NumberToString(
  249. blink::features::kBrowsingTopicsConfigVersion.Get())});
  250. result_topic->model_version = base::NumberToString(epoch->model_version());
  251. result_topic->taxonomy_version =
  252. base::NumberToString(epoch->taxonomy_version());
  253. result_topic->version = base::StrCat({result_topic->config_version, ":",
  254. result_topic->taxonomy_version, ":",
  255. result_topic->model_version});
  256. topics_with_status.emplace_back(std::move(result_topic),
  257. output_is_true_topic);
  258. }
  259. // Sort `topics_with_status` based on `EpochTopicPtr` first, and if the
  260. // `EpochTopicPtr` parts are equal, then a true topic will be ordered before a
  261. // random topic. This ensures that when we later deduplicate based on the
  262. // `EpochTopicPtr` field only, the associated is-true-topic status will be
  263. // true as long as there is one true topic for that topic in
  264. // `topics_with_status`.
  265. std::sort(topics_with_status.begin(), topics_with_status.end(),
  266. [](const auto& left, const auto& right) {
  267. if (left.first < right.first)
  268. return true;
  269. if (left.first > right.first)
  270. return false;
  271. return right.second < left.second;
  272. });
  273. // Remove duplicate `EpochTopicPtr` entries.
  274. topics_with_status.erase(
  275. std::unique(topics_with_status.begin(), topics_with_status.end(),
  276. [](const auto& left, const auto& right) {
  277. return left.first == right.first;
  278. }),
  279. topics_with_status.end());
  280. // Shuffle the entries.
  281. base::RandomShuffle(topics_with_status.begin(), topics_with_status.end());
  282. if (topics_with_status.empty()) {
  283. if (has_filtered_topics) {
  284. RecordBrowsingTopicsApiResultUkmMetrics(
  285. EmptyApiResultReason::kCandicateTopicsFiltered, main_frame);
  286. } else {
  287. RecordBrowsingTopicsApiResultUkmMetrics(
  288. EmptyApiResultReason::kNoCandicateTopics, main_frame);
  289. }
  290. return {};
  291. }
  292. RecordBrowsingTopicsApiResultUkmMetrics(topics_with_status, main_frame);
  293. std::vector<blink::mojom::EpochTopicPtr> result_topics;
  294. result_topics.reserve(topics_with_status.size());
  295. std::transform(topics_with_status.begin(), topics_with_status.end(),
  296. std::back_inserter(result_topics),
  297. [](auto& topic_with_status) {
  298. return std::move(topic_with_status.first);
  299. });
  300. return result_topics;
  301. }
  302. void BrowsingTopicsServiceImpl::GetBrowsingTopicsStateForWebUi(
  303. bool calculate_now,
  304. mojom::PageHandler::GetBrowsingTopicsStateCallback callback) {
  305. if (!browsing_topics_state_loaded_) {
  306. std::move(callback).Run(
  307. mojom::WebUIGetBrowsingTopicsStateResult::NewOverrideStatusMessage(
  308. "State loading hasn't finished. Please retry shortly."));
  309. return;
  310. }
  311. // If a calculation is already in progress, get the webui topics state after
  312. // the calculation is done. Do this regardless of whether `calculate_now` is
  313. // true, i.e. if `calculate_now` is true, this request is effectively merged
  314. // with the in progress calculation.
  315. if (topics_calculator_) {
  316. get_state_for_webui_callbacks_.push_back(std::move(callback));
  317. return;
  318. }
  319. DCHECK(schedule_calculate_timer_.IsRunning());
  320. if (calculate_now) {
  321. get_state_for_webui_callbacks_.push_back(std::move(callback));
  322. schedule_calculate_timer_.AbandonAndStop();
  323. CalculateBrowsingTopics();
  324. return;
  325. }
  326. std::move(callback).Run(GetBrowsingTopicsStateForWebUiHelper());
  327. }
  328. std::vector<privacy_sandbox::CanonicalTopic>
  329. BrowsingTopicsServiceImpl::GetTopicsForSiteForDisplay(
  330. const url::Origin& top_origin) const {
  331. if (!browsing_topics_state_loaded_)
  332. return {};
  333. std::string top_domain =
  334. net::registry_controlled_domains::GetDomainAndRegistry(
  335. top_origin.GetURL(),
  336. net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  337. std::vector<privacy_sandbox::CanonicalTopic> result;
  338. for (const EpochTopics* epoch :
  339. browsing_topics_state_.EpochsForSite(top_domain)) {
  340. absl::optional<Topic> topic = epoch->TopicForSiteForDisplay(
  341. top_domain, browsing_topics_state_.hmac_key());
  342. if (!topic)
  343. continue;
  344. // `epoch->TopicForSiteForDisplay()` shall only return a top topic, and a
  345. // top topic can never be in the disallowed state (i.e. it will be cleared
  346. // when it becomes diallowed).
  347. DCHECK(privacy_sandbox_settings_->IsTopicAllowed(
  348. privacy_sandbox::CanonicalTopic(*topic, epoch->taxonomy_version())));
  349. result.emplace_back(*topic, epoch->taxonomy_version());
  350. }
  351. return result;
  352. }
  353. std::vector<privacy_sandbox::CanonicalTopic>
  354. BrowsingTopicsServiceImpl::GetTopTopicsForDisplay() const {
  355. if (!browsing_topics_state_loaded_)
  356. return {};
  357. std::vector<privacy_sandbox::CanonicalTopic> result;
  358. for (const EpochTopics& epoch : browsing_topics_state_.epochs()) {
  359. DCHECK_LE(epoch.padded_top_topics_start_index(),
  360. epoch.top_topics_and_observing_domains().size());
  361. for (size_t i = 0; i < epoch.padded_top_topics_start_index(); ++i) {
  362. const TopicAndDomains& topic_and_domains =
  363. epoch.top_topics_and_observing_domains()[i];
  364. if (!topic_and_domains.IsValid())
  365. continue;
  366. // A top topic can never be in the disallowed state (i.e. it will be
  367. // cleared when it becomes diallowed).
  368. DCHECK(privacy_sandbox_settings_->IsTopicAllowed(
  369. privacy_sandbox::CanonicalTopic(topic_and_domains.topic(),
  370. epoch.taxonomy_version())));
  371. result.emplace_back(topic_and_domains.topic(), epoch.taxonomy_version());
  372. }
  373. }
  374. return result;
  375. }
  376. void BrowsingTopicsServiceImpl::ClearTopic(
  377. const privacy_sandbox::CanonicalTopic& canonical_topic) {
  378. if (!browsing_topics_state_loaded_)
  379. return;
  380. browsing_topics_state_.ClearTopic(canonical_topic.topic_id(),
  381. canonical_topic.taxonomy_version());
  382. }
  383. void BrowsingTopicsServiceImpl::ClearTopicsDataForOrigin(
  384. const url::Origin& origin) {
  385. if (!browsing_topics_state_loaded_)
  386. return;
  387. std::string context_domain =
  388. net::registry_controlled_domains::GetDomainAndRegistry(
  389. origin.GetURL(),
  390. net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  391. HashedDomain hashed_context_domain = HashContextDomainForStorage(
  392. browsing_topics_state_.hmac_key(), context_domain);
  393. browsing_topics_state_.ClearContextDomain(hashed_context_domain);
  394. site_data_manager_->ClearContextDomain(hashed_context_domain);
  395. }
  396. void BrowsingTopicsServiceImpl::ClearAllTopicsData() {
  397. if (!browsing_topics_state_loaded_)
  398. return;
  399. browsing_topics_state_.ClearAllTopics();
  400. site_data_manager_->ExpireDataBefore(base::Time::Now());
  401. }
  402. std::unique_ptr<BrowsingTopicsCalculator>
  403. BrowsingTopicsServiceImpl::CreateCalculator(
  404. privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings,
  405. history::HistoryService* history_service,
  406. content::BrowsingTopicsSiteDataManager* site_data_manager,
  407. optimization_guide::PageContentAnnotationsService* annotations_service,
  408. const base::circular_deque<EpochTopics>& epochs,
  409. BrowsingTopicsCalculator::CalculateCompletedCallback callback) {
  410. return std::make_unique<BrowsingTopicsCalculator>(
  411. privacy_sandbox_settings, history_service, site_data_manager,
  412. annotations_service, epochs, std::move(callback));
  413. }
  414. const BrowsingTopicsState& BrowsingTopicsServiceImpl::browsing_topics_state() {
  415. return browsing_topics_state_;
  416. }
  417. void BrowsingTopicsServiceImpl::ScheduleBrowsingTopicsCalculation(
  418. base::TimeDelta delay) {
  419. DCHECK(browsing_topics_state_loaded_);
  420. // `this` owns the timer, which is automatically cancelled on destruction, so
  421. // base::Unretained(this) is safe.
  422. schedule_calculate_timer_.Start(
  423. FROM_HERE, delay,
  424. base::BindOnce(&BrowsingTopicsServiceImpl::CalculateBrowsingTopics,
  425. base::Unretained(this)));
  426. }
  427. void BrowsingTopicsServiceImpl::CalculateBrowsingTopics() {
  428. DCHECK(browsing_topics_state_loaded_);
  429. DCHECK(!topics_calculator_);
  430. // `this` owns `topics_calculator_` so `topics_calculator_` should not invoke
  431. // the callback once it's destroyed.
  432. topics_calculator_ = CreateCalculator(
  433. privacy_sandbox_settings_, history_service_, site_data_manager_,
  434. annotations_service_, browsing_topics_state_.epochs(),
  435. base::BindOnce(
  436. &BrowsingTopicsServiceImpl::OnCalculateBrowsingTopicsCompleted,
  437. base::Unretained(this)));
  438. }
  439. void BrowsingTopicsServiceImpl::OnCalculateBrowsingTopicsCompleted(
  440. EpochTopics epoch_topics) {
  441. DCHECK(browsing_topics_state_loaded_);
  442. DCHECK(topics_calculator_);
  443. topics_calculator_.reset();
  444. browsing_topics_state_.AddEpoch(std::move(epoch_topics));
  445. browsing_topics_state_.UpdateNextScheduledCalculationTime();
  446. ScheduleBrowsingTopicsCalculation(
  447. blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get());
  448. if (!get_state_for_webui_callbacks_.empty()) {
  449. mojom::WebUIGetBrowsingTopicsStateResultPtr webui_state =
  450. GetBrowsingTopicsStateForWebUiHelper();
  451. for (auto& callback : get_state_for_webui_callbacks_) {
  452. std::move(callback).Run(webui_state->Clone());
  453. }
  454. get_state_for_webui_callbacks_.clear();
  455. }
  456. }
  457. void BrowsingTopicsServiceImpl::OnBrowsingTopicsStateLoaded() {
  458. DCHECK(!browsing_topics_state_loaded_);
  459. browsing_topics_state_loaded_ = true;
  460. base::Time browsing_topics_data_sccessible_since =
  461. privacy_sandbox_settings_->TopicsDataAccessibleSince();
  462. StartupCalculateDecision decision = GetStartupCalculationDecision(
  463. browsing_topics_state_, browsing_topics_data_sccessible_since,
  464. base::BindRepeating(
  465. &privacy_sandbox::PrivacySandboxSettings::IsTopicAllowed,
  466. base::Unretained(privacy_sandbox_settings_)));
  467. if (decision.clear_topics_data)
  468. browsing_topics_state_.ClearAllTopics();
  469. site_data_manager_->ExpireDataBefore(browsing_topics_data_sccessible_since);
  470. ScheduleBrowsingTopicsCalculation(decision.next_calculation_delay);
  471. }
  472. void BrowsingTopicsServiceImpl::Shutdown() {
  473. privacy_sandbox_settings_observation_.Reset();
  474. history_service_observation_.Reset();
  475. }
  476. void BrowsingTopicsServiceImpl::OnTopicsDataAccessibleSinceUpdated() {
  477. if (!browsing_topics_state_loaded_)
  478. return;
  479. // Here we rely on the fact that `browsing_topics_data_accessible_since` can
  480. // only be updated to base::Time::Now() due to data deletion. In this case, we
  481. // should just clear all topics.
  482. browsing_topics_state_.ClearAllTopics();
  483. site_data_manager_->ExpireDataBefore(
  484. privacy_sandbox_settings_->TopicsDataAccessibleSince());
  485. // Abort the outstanding topics calculation and restart immediately.
  486. if (topics_calculator_) {
  487. DCHECK(!schedule_calculate_timer_.IsRunning());
  488. topics_calculator_.reset();
  489. CalculateBrowsingTopics();
  490. }
  491. }
  492. void BrowsingTopicsServiceImpl::OnURLsDeleted(
  493. history::HistoryService* history_service,
  494. const history::DeletionInfo& deletion_info) {
  495. if (!browsing_topics_state_loaded_)
  496. return;
  497. // Ignore invalid time_range.
  498. if (!deletion_info.IsAllHistory() && !deletion_info.time_range().IsValid())
  499. return;
  500. for (size_t i = 0; i < browsing_topics_state_.epochs().size(); ++i) {
  501. const EpochTopics& epoch_topics = browsing_topics_state_.epochs()[i];
  502. if (epoch_topics.empty())
  503. continue;
  504. // The typical case is assumed here. We cannot always derive the original
  505. // history start time, as the necessary data (e.g. its previous epoch's
  506. // calculation time) may have been gone.
  507. base::Time history_data_start_time =
  508. epoch_topics.calculation_time() -
  509. blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get();
  510. bool time_range_overlap =
  511. epoch_topics.calculation_time() >= deletion_info.time_range().begin() &&
  512. history_data_start_time <= deletion_info.time_range().end();
  513. if (time_range_overlap)
  514. browsing_topics_state_.ClearOneEpoch(i);
  515. }
  516. // If there's an outstanding topics calculation, abort and restart it.
  517. if (topics_calculator_) {
  518. DCHECK(!schedule_calculate_timer_.IsRunning());
  519. topics_calculator_.reset();
  520. CalculateBrowsingTopics();
  521. }
  522. }
  523. mojom::WebUIGetBrowsingTopicsStateResultPtr
  524. BrowsingTopicsServiceImpl::GetBrowsingTopicsStateForWebUiHelper() {
  525. DCHECK(browsing_topics_state_loaded_);
  526. DCHECK(!topics_calculator_);
  527. auto webui_state = mojom::WebUIBrowsingTopicsState::New();
  528. webui_state->next_scheduled_calculation_time =
  529. browsing_topics_state_.next_scheduled_calculation_time();
  530. for (const EpochTopics& epoch : browsing_topics_state_.epochs()) {
  531. DCHECK_LE(epoch.padded_top_topics_start_index(),
  532. epoch.top_topics_and_observing_domains().size());
  533. // Note: for a failed epoch calculation, the default zero-initialized values
  534. // will be displayed in the Web UI.
  535. auto webui_epoch = mojom::WebUIEpoch::New();
  536. webui_epoch->calculation_time = epoch.calculation_time();
  537. webui_epoch->model_version = base::NumberToString(epoch.model_version());
  538. webui_epoch->taxonomy_version =
  539. base::NumberToString(epoch.taxonomy_version());
  540. for (size_t i = 0; i < epoch.top_topics_and_observing_domains().size();
  541. ++i) {
  542. const TopicAndDomains& topic_and_domains =
  543. epoch.top_topics_and_observing_domains()[i];
  544. privacy_sandbox::CanonicalTopic canonical_topic =
  545. privacy_sandbox::CanonicalTopic(topic_and_domains.topic(),
  546. epoch.taxonomy_version());
  547. std::vector<std::string> webui_observed_by_domains;
  548. webui_observed_by_domains.reserve(
  549. topic_and_domains.hashed_domains().size());
  550. for (const auto& domain : topic_and_domains.hashed_domains()) {
  551. webui_observed_by_domains.push_back(
  552. base::NumberToString(domain.value()));
  553. }
  554. // Note: if the topic is invalid (i.e. cleared), the output `topic_id`
  555. // will be 0; if the topic is invalid, or if the taxonomy version isn't
  556. // recognized by this Chrome binary, the output `topic_name` will be
  557. // "Unknown".
  558. auto webui_topic = mojom::WebUITopic::New();
  559. webui_topic->topic_id = topic_and_domains.topic().value();
  560. webui_topic->topic_name = canonical_topic.GetLocalizedRepresentation();
  561. webui_topic->is_real_topic = (i < epoch.padded_top_topics_start_index());
  562. webui_topic->observed_by_domains = std::move(webui_observed_by_domains);
  563. webui_epoch->topics.push_back(std::move(webui_topic));
  564. }
  565. webui_state->epochs.push_back(std::move(webui_epoch));
  566. }
  567. // Reorder the epochs from latest to oldest.
  568. base::ranges::reverse(webui_state->epochs);
  569. return mojom::WebUIGetBrowsingTopicsStateResult::NewBrowsingTopicsState(
  570. std::move(webui_state));
  571. }
  572. } // namespace browsing_topics