blob: 46bc295343b6524b648f071805344a554cfc3680 [file] [log] [blame]
// 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 "chrome/browser/content_index/content_index_provider_impl.h"
#include <memory>
#include "base/barrier_closure.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/post_task.h"
#include "build/build_config.h"
#include "chrome/browser/engagement/site_engagement_score.h"
#include "chrome/browser/engagement/site_engagement_service.h"
#include "chrome/browser/engagement/site_engagement_service_factory.h"
#include "chrome/browser/metrics/ukm_background_recorder_service.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/offline_items_collection/core/update_delta.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_index_context.h"
#include "content/public/browser/storage_partition.h"
#include "ui/gfx/image/image_skia.h"
#include "url/origin.h"
#if defined(OS_ANDROID)
#include "chrome/browser/android/service_tab_launcher.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/common/referrer.h"
#else
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#endif
using offline_items_collection::ContentId;
using offline_items_collection::LaunchLocation;
using offline_items_collection::OfflineItem;
using offline_items_collection::OfflineItemFilter;
namespace {
constexpr char kProviderNamespace[] = "content_index";
constexpr char kEntryKeySeparator[] = "#";
struct EntryKeyComponents {
int64_t service_worker_registration_id;
url::Origin origin;
std::string description_id;
};
std::string EntryKey(int64_t service_worker_registration_id,
const url::Origin& origin,
const std::string& description_id) {
return base::NumberToString(service_worker_registration_id) +
kEntryKeySeparator + origin.GetURL().spec() + kEntryKeySeparator +
description_id;
}
std::string EntryKey(const content::ContentIndexEntry& entry) {
return EntryKey(entry.service_worker_registration_id,
url::Origin::Create(entry.launch_url.GetOrigin()),
entry.description->id);
}
EntryKeyComponents GetEntryKeyComponents(const std::string& key) {
size_t pos1 = key.find_first_of(kEntryKeySeparator);
DCHECK_NE(pos1, std::string::npos);
size_t pos2 = key.find_first_of(kEntryKeySeparator, pos1 + 1);
DCHECK_NE(pos2, std::string::npos);
int64_t service_worker_registration_id = -1;
base::StringToInt64(base::StringPiece(key.data(), pos1),
&service_worker_registration_id);
GURL origin(key.substr(pos1 + 1, pos2 - pos1 - 1));
DCHECK(origin.is_valid());
return {service_worker_registration_id, url::Origin::Create(origin),
key.substr(pos2 + 1)};
}
OfflineItemFilter CategoryToFilter(blink::mojom::ContentCategory category) {
switch (category) {
case blink::mojom::ContentCategory::NONE:
case blink::mojom::ContentCategory::HOME_PAGE:
case blink::mojom::ContentCategory::ARTICLE:
return OfflineItemFilter::FILTER_PAGE;
case blink::mojom::ContentCategory::VIDEO:
return OfflineItemFilter::FILTER_VIDEO;
case blink::mojom::ContentCategory::AUDIO:
return OfflineItemFilter::FILTER_AUDIO;
}
}
} // namespace
ContentIndexProviderImpl::ContentIndexProviderImpl(Profile* profile)
: profile_(profile),
metrics_(ukm::UkmBackgroundRecorderFactory::GetForProfile(profile)),
aggregator_(
OfflineContentAggregatorFactory::GetForKey(profile->GetProfileKey())),
site_engagement_service_(
SiteEngagementServiceFactory::GetForProfile(profile)) {
aggregator_->RegisterProvider(kProviderNamespace, this);
}
ContentIndexProviderImpl::~ContentIndexProviderImpl() {
if (aggregator_)
aggregator_->UnregisterProvider(kProviderNamespace);
}
void ContentIndexProviderImpl::Shutdown() {
aggregator_->UnregisterProvider(kProviderNamespace);
aggregator_ = nullptr;
site_engagement_service_ = nullptr;
}
std::vector<gfx::Size> ContentIndexProviderImpl::GetIconSizes(
blink::mojom::ContentCategory category) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (icon_sizes_for_testing_)
return *icon_sizes_for_testing_;
#if defined(OS_ANDROID)
// Recommended notification icon size for Android.
return {{192, 192}};
#else
return {};
#endif
}
void ContentIndexProviderImpl::OnContentAdded(
content::ContentIndexEntry entry) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
OfflineItemList items(1, EntryToOfflineItem(entry));
// Delete the entry before adding it just in case the ID was overwritten.
for (auto& observer : observers_)
observer.OnItemRemoved(items[0].id);
for (auto& observer : observers_)
observer.OnItemsAdded(items);
metrics_.RecordContentAdded(url::Origin::Create(entry.launch_url.GetOrigin()),
entry.description->category);
}
void ContentIndexProviderImpl::OnContentDeleted(
int64_t service_worker_registration_id,
const url::Origin& origin,
const std::string& description_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::string entry_key =
EntryKey(service_worker_registration_id, origin, description_id);
ContentId id(kProviderNamespace, entry_key);
for (auto& observer : observers_)
observer.OnItemRemoved(id);
}
void ContentIndexProviderImpl::OpenItem(
const offline_items_collection::OpenParams& open_params,
const ContentId& id) {
auto components = GetEntryKeyComponents(id.id);
auto* storage_partition = content::BrowserContext::GetStoragePartitionForSite(
profile_, components.origin.GetURL(), /* can_create= */ false);
if (!storage_partition || !storage_partition->GetContentIndexContext())
return;
storage_partition->GetContentIndexContext()->GetEntry(
components.service_worker_registration_id, components.description_id,
base::BindOnce(&ContentIndexProviderImpl::DidGetEntryToOpen,
weak_ptr_factory_.GetWeakPtr()));
}
void ContentIndexProviderImpl::DidGetEntryToOpen(
base::Optional<content::ContentIndexEntry> entry) {
if (!entry)
return;
#if defined(OS_ANDROID)
content::OpenURLParams params(entry->launch_url, content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_LINK,
/* is_renderer_initiated= */ false);
ServiceTabLauncher::GetInstance()->LaunchTab(
profile_, params,
base::BindOnce(&ContentIndexProviderImpl::DidOpenTab,
weak_ptr_factory_.GetWeakPtr(), std::move(*entry)));
#else
NavigateParams nav_params(profile_, entry->launch_url,
ui::PAGE_TRANSITION_LINK);
Navigate(&nav_params);
DidOpenTab(std::move(*entry), nav_params.navigated_or_inserted_contents);
#endif
}
void ContentIndexProviderImpl::DidOpenTab(content::ContentIndexEntry entry,
content::WebContents* web_contents) {
metrics_.RecordContentOpened(web_contents, entry.description->category);
}
void ContentIndexProviderImpl::RemoveItem(const ContentId& id) {
auto components = GetEntryKeyComponents(id.id);
auto* storage_partition = content::BrowserContext::GetStoragePartitionForSite(
profile_, components.origin.GetURL(), /* can_create= */ false);
if (!storage_partition || !storage_partition->GetContentIndexContext())
return;
metrics_.RecordContentDeletedByUser(components.origin);
storage_partition->GetContentIndexContext()->OnUserDeletedItem(
components.service_worker_registration_id, components.origin,
components.description_id);
}
void ContentIndexProviderImpl::CancelDownload(const ContentId& id) {
NOTREACHED();
}
void ContentIndexProviderImpl::PauseDownload(const ContentId& id) {
NOTREACHED();
}
void ContentIndexProviderImpl::ResumeDownload(const ContentId& id,
bool has_user_gesture) {
NOTREACHED();
}
void ContentIndexProviderImpl::GetItemById(const ContentId& id,
SingleItemCallback callback) {
auto components = GetEntryKeyComponents(id.id);
auto* storage_partition = content::BrowserContext::GetStoragePartitionForSite(
profile_, components.origin.GetURL(), /* can_create= */ false);
if (!storage_partition || !storage_partition->GetContentIndexContext()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), base::nullopt));
return;
}
storage_partition->GetContentIndexContext()->GetEntry(
components.service_worker_registration_id, components.description_id,
base::BindOnce(&ContentIndexProviderImpl::DidGetItem,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ContentIndexProviderImpl::DidGetItem(
SingleItemCallback callback,
base::Optional<content::ContentIndexEntry> entry) {
if (!entry)
std::move(callback).Run(base::nullopt);
else
std::move(callback).Run(EntryToOfflineItem(*entry));
}
void ContentIndexProviderImpl::GetAllItems(MultipleItemCallback callback) {
// Get the number of Storage Paritions.
std::vector<content::StoragePartition*> storage_paritions;
content::BrowserContext::ForEachStoragePartition(
profile_,
base::BindRepeating(
[](std::vector<content::StoragePartition*>* storage_paritions,
content::StoragePartition* storage_partition) {
storage_paritions->push_back(storage_partition);
},
&storage_paritions));
DCHECK(!storage_paritions.empty());
auto item_list = std::make_unique<OfflineItemList>();
OfflineItemList* item_list_ptr = item_list.get();
// Get the all entries from every partition.
auto barrier_closure = base::BarrierClosure(
storage_paritions.size(),
base::BindOnce(
&ContentIndexProviderImpl::DidGetAllEntriesAcrossStorageParitions,
weak_ptr_factory_.GetWeakPtr(), std::move(item_list),
std::move(callback)));
for (auto* storage_partition : storage_paritions) {
if (!storage_partition || !storage_partition->GetContentIndexContext()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, barrier_closure);
continue;
}
// |item_list_ptr| is safe to use since it is owned by the barrier
// closure.
storage_partition->GetContentIndexContext()->GetAllEntries(base::BindOnce(
&ContentIndexProviderImpl::DidGetAllEntries,
weak_ptr_factory_.GetWeakPtr(), barrier_closure, item_list_ptr));
}
}
void ContentIndexProviderImpl::DidGetAllEntriesAcrossStorageParitions(
std::unique_ptr<OfflineItemList> item_list,
MultipleItemCallback callback) {
ContentIndexMetrics::RecordContentIndexEntries(item_list->size());
std::move(callback).Run(*item_list);
}
void ContentIndexProviderImpl::DidGetAllEntries(
base::OnceClosure done_closure,
OfflineItemList* item_list,
blink::mojom::ContentIndexError error,
std::vector<content::ContentIndexEntry> entries) {
if (error != blink::mojom::ContentIndexError::NONE) {
std::move(done_closure).Run();
return;
}
for (const auto& entry : entries)
item_list->push_back(EntryToOfflineItem(entry));
std::move(done_closure).Run();
}
void ContentIndexProviderImpl::GetVisualsForItem(const ContentId& id,
GetVisualsOptions options,
VisualsCallback callback) {
auto components = GetEntryKeyComponents(id.id);
auto* storage_partition = content::BrowserContext::GetStoragePartitionForSite(
profile_, components.origin.GetURL(), /* can_create= */ false);
if (!storage_partition || !storage_partition->GetContentIndexContext()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), id, nullptr));
return;
}
storage_partition->GetContentIndexContext()->GetIcons(
components.service_worker_registration_id, components.description_id,
base::BindOnce(&ContentIndexProviderImpl::DidGetIcons,
weak_ptr_factory_.GetWeakPtr(), id, std::move(callback)));
}
OfflineItem ContentIndexProviderImpl::EntryToOfflineItem(
const content::ContentIndexEntry& entry) {
OfflineItem item;
item.id = ContentId(kProviderNamespace, EntryKey(entry));
item.title = entry.description->title;
item.description = entry.description->description;
item.filter = CategoryToFilter(entry.description->category);
item.is_transient = false;
item.is_suggested = true;
item.creation_time = entry.registration_time;
item.is_openable = true;
item.state = offline_items_collection::OfflineItemState::COMPLETE;
item.is_resumable = false;
item.can_rename = false;
item.page_url = entry.launch_url;
if (site_engagement_service_) {
item.content_quality_score =
site_engagement_service_->GetScore(entry.launch_url.GetOrigin()) /
SiteEngagementScore::kMaxPoints;
}
return item;
}
void ContentIndexProviderImpl::DidGetIcons(const ContentId& id,
VisualsCallback callback,
std::vector<SkBitmap> icons) {
auto visuals =
std::make_unique<offline_items_collection::OfflineItemVisuals>();
if (!icons.empty()) {
DCHECK_EQ(icons.size(), 1u);
visuals->icon = gfx::Image::CreateFrom1xBitmap(std::move(icons.front()));
}
std::move(callback).Run(id, std::move(visuals));
}
void ContentIndexProviderImpl::GetShareInfoForItem(const ContentId& id,
ShareCallback callback) {
NOTIMPLEMENTED();
}
void ContentIndexProviderImpl::RenameItem(const ContentId& id,
const std::string& name,
RenameCallback callback) {
NOTREACHED();
}
void ContentIndexProviderImpl::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void ContentIndexProviderImpl::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}