blob: 9123d0f002e56c1facc252b9953cff71195790d3 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <utility>
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "chrome/browser/apps/almanac_api_client/device_info_manager.h"
#include "chrome/browser/apps/app_deduplication_service/app_deduplication_service.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "components/services/app_service/public/cpp/types_util.h"
namespace {
// Relative file path to where the deduplication data will be stored on disk.
constexpr char kAppDeduplicationFilePath[] =
"app_deduplication_service/deduplication_data/deduplication_data.pb";
// Converts PackageId strings to Entrys when the source is Website.
std::optional<apps::deduplication::Entry> GetEntryForWebsite(
const std::string& id) {
size_t separator = id.find_first_of(':');
apps::deduplication::Entry entry;
if (separator == std::string::npos || separator == id.size() - 1) {
LOG(ERROR) << "Source is an unsupported type.";
return std::nullopt;
}
std::string app_type = id.substr(0, separator);
std::string app_id = id.substr(separator + 1);
GURL entry_url = GURL(app_id);
if (entry_url.is_valid() && app_type == "website") {
entry = apps::deduplication::Entry(entry_url);
} else {
LOG(ERROR) << "Source is an unsupported type.";
return std::nullopt;
}
return entry;
}
} // namespace
namespace apps::deduplication {
namespace prefs {
constexpr char kLastGetDataFromServerTimestamp[] =
"apps.app_deduplication_service.last_get_data_from_server_timestamp";
} // namespace prefs
AppDeduplicationService::AppDeduplicationService(Profile* profile)
: profile_(profile),
server_connector_(std::make_unique<AppDeduplicationServerConnector>()),
device_info_manager_(std::make_unique<DeviceInfoManager>(profile)) {
app_registry_cache_observation_.Observe(
&apps::AppServiceProxyFactory::GetForProfile(profile)
->AppRegistryCache());
if (base::FeatureList::IsEnabled(features::kAppDeduplicationServiceFondue)) {
base::FilePath path =
profile_->GetPath().AppendASCII(kAppDeduplicationFilePath);
proto_file_manager_ =
std::make_unique<ProtoFileManager<proto::DeduplicateData>>(path);
StartLoginFlow();
}
}
AppDeduplicationService::~AppDeduplicationService() = default;
bool AppDeduplicationService::IsServiceOn() {
return !duplication_map_.empty();
}
void AppDeduplicationService::StartLoginFlow() {
const int hours_diff =
std::abs((GetServerPref() - base::Time::Now()).InHours());
if (hours_diff >= 24) {
device_info_manager_->GetDeviceInfo(
base::BindOnce(&AppDeduplicationService::GetDeduplicateDataFromServer,
weak_ptr_factory_.GetWeakPtr()));
} else {
// Read most recent data from cache.
proto_file_manager_->ReadProtoFromFile(base::BindOnce(
&AppDeduplicationService::OnReadDeduplicationCacheCompleted,
weak_ptr_factory_.GetWeakPtr()));
}
}
void AppDeduplicationService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterTimePref(prefs::kLastGetDataFromServerTimestamp,
base::Time());
}
std::vector<Entry> AppDeduplicationService::GetDuplicates(
const Entry& entry_query) {
std::vector<Entry> entries;
std::optional<uint32_t> duplication_index = FindDuplicationIndex(entry_query);
if (!duplication_index.has_value()) {
return entries;
}
const auto& group = duplication_map_.find(duplication_index.value());
if (group == duplication_map_.end()) {
return entries;
}
for (const auto& entry : group->second.entries) {
if (entry.entry_status == EntryStatus::kNonApp ||
entry.entry_status == EntryStatus::kInstalledApp) {
entries.push_back(entry);
}
}
return entries;
}
bool AppDeduplicationService::AreDuplicates(const Entry& entry_1,
const Entry& entry_2) {
// TODO(b/238394602): Add interface with more than 2 entry ids.
std::optional<uint32_t> duplication_index_1 = FindDuplicationIndex(entry_1);
if (!duplication_index_1.has_value()) {
return false;
}
std::optional<uint32_t> duplication_index_2 = FindDuplicationIndex(entry_2);
if (!duplication_index_2.has_value()) {
return false;
}
return duplication_index_1 == duplication_index_2;
}
void AppDeduplicationService::OnAppUpdate(const apps::AppUpdate& update) {
UpdateInstallationStatus(update);
}
void AppDeduplicationService::OnAppRegistryCacheWillBeDestroyed(
apps::AppRegistryCache* cache) {
app_registry_cache_observation_.Reset();
}
void AppDeduplicationService::UpdateInstallationStatus(
const apps::AppUpdate& update) {
Entry entry(update.PublisherId(), update.AppType());
entry.entry_status = apps_util::IsInstalled(update.Readiness())
? EntryStatus::kInstalledApp
: EntryStatus::kNotInstalledApp;
}
std::optional<uint32_t> AppDeduplicationService::FindDuplicationIndex(
const Entry& entry) {
// TODO(b/238394602): Add logic to handle url entry id and web apps.
// Check if there is an exact match of the entry id.
auto it = entry_to_group_map_.find(entry);
if (it != entry_to_group_map_.end()) {
return it->second;
}
// For website, check if the url is in the scope of the recorded url in the
// deduplication database. Here we assume all the websites has it's own entry.
GURL entry_url = GURL(entry.id);
if (entry.entry_type == EntryType::kWebPage && entry_url.is_valid()) {
for (const auto& [recorded_entry, group_id] : entry_to_group_map_) {
if (recorded_entry.entry_type != EntryType::kWebPage) {
continue;
}
GURL recorded_entry_url = GURL(recorded_entry.id);
if (!recorded_entry_url.is_valid()) {
continue;
}
if (entry_url.scheme().empty() || recorded_entry_url.scheme().empty() ||
entry_url.scheme() != recorded_entry_url.scheme()) {
continue;
}
if (entry_url.host().empty() || recorded_entry_url.host().empty() ||
entry_url.host() != recorded_entry_url.host()) {
continue;
}
if (!entry_url.has_path() || !recorded_entry_url.has_path()) {
continue;
}
if (base::StartsWith(entry_url.path(), recorded_entry_url.path(),
base::CompareCase::INSENSITIVE_ASCII)) {
return group_id;
}
}
}
return std::nullopt;
}
void AppDeduplicationService::GetDeduplicateDataFromServer(
DeviceInfo device_info) {
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
profile_->GetURLLoaderFactory();
if (!url_loader_factory.get()) {
// `url_loader_factory` should only be null if we are in a non-dedupe
// related test. Tests that use profile builder to create their profile
// won't have `url_loader_factory` set up by default, so we bypass dedupes
// code being called for those tests.
CHECK_IS_TEST();
return;
}
server_connector_->GetDeduplicateAppsFromServer(
device_info, url_loader_factory,
base::BindOnce(
&AppDeduplicationService::OnGetDeduplicateDataFromServerCompleted,
weak_ptr_factory_.GetWeakPtr()));
}
void AppDeduplicationService::OnGetDeduplicateDataFromServerCompleted(
std::optional<proto::DeduplicateData> response) {
if (response.has_value()) {
profile_->GetPrefs()->SetTime(prefs::kLastGetDataFromServerTimestamp,
base::Time::Now());
proto_file_manager_->WriteProtoToFile(
response.value(),
base::BindOnce(
&AppDeduplicationService::OnWriteDeduplicationCacheCompleted,
weak_ptr_factory_.GetWeakPtr()));
} else {
proto_file_manager_->ReadProtoFromFile(base::BindOnce(
&AppDeduplicationService::OnReadDeduplicationCacheCompleted,
weak_ptr_factory_.GetWeakPtr()));
}
if (get_data_complete_callback_for_testing_) {
std::move(get_data_complete_callback_for_testing_)
.Run(response.has_value());
}
}
void AppDeduplicationService::OnWriteDeduplicationCacheCompleted(bool result) {
if (!result) {
LOG(ERROR) << "Writing deduplication data to disk failed.";
return;
}
proto_file_manager_->ReadProtoFromFile(base::BindOnce(
&AppDeduplicationService::OnReadDeduplicationCacheCompleted,
weak_ptr_factory_.GetWeakPtr()));
return;
}
void AppDeduplicationService::OnReadDeduplicationCacheCompleted(
std::optional<proto::DeduplicateData> data) {
if (!data.has_value()) {
LOG(ERROR) << "Reading deduplication data from disk failed.";
return;
}
DeduplicateDataToEntries(data.value());
}
base::Time AppDeduplicationService::GetServerPref() {
return profile_->GetPrefs()->GetTime(prefs::kLastGetDataFromServerTimestamp);
}
// This function is only used when the kAppDeduplicationServiceFondue flag
// is enabled.
void AppDeduplicationService::DeduplicateDataToEntries(
const proto::DeduplicateData data) {
// Use the index as the internal indexing key for fast look up. If the
// size of the duplicated groups goes over integer 32 limit, a new indexing
// key needs to be introduced.
uint32_t index = 1;
for (auto const& group : data.app_group()) {
DuplicateGroup duplicate_group;
for (auto const& id : group.package_id()) {
std::optional<PackageId> package_id = PackageId::FromString(id);
std::string app_id;
Entry entry;
if (!package_id.has_value()) {
std::optional<Entry> web_id = GetEntryForWebsite(id);
if (!web_id.has_value()) {
continue;
}
entry = web_id.value();
} else {
AppType source = package_id.value().app_type();
app_id = package_id.value().identifier();
if (source != AppType::kArc && source != AppType::kWeb) {
LOG(ERROR) << "Source is an unsupported type.";
NOTREACHED();
}
entry = Entry(app_id, source);
}
// Initialize entry status.
entry.entry_status = entry.entry_type == EntryType::kApp
? EntryStatus::kNotInstalledApp
: EntryStatus::kNonApp;
entry_to_group_map_[entry] = index;
duplicate_group.entries.push_back(std::move(entry));
}
if (!duplicate_group.entries.empty()) {
duplication_map_[index] = std::move(duplicate_group);
index++;
}
}
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile_);
proxy->AppRegistryCache().ForEachApp([this](const apps::AppUpdate& update) {
UpdateInstallationStatus(update);
});
}
} // namespace apps::deduplication