blob: 7bf609921a25093e7b60ffa88f545daafc58cc20 [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/web_applications/web_app_sync_bridge.h"
#include <memory>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/optional.h"
#include "base/util/type_safety/pass_key.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_database.h"
#include "chrome/browser/web_applications/web_app_database_factory.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_install_delegate.h"
#include "chrome/common/channel_info.h"
#include "components/sync/base/model_type.h"
#include "components/sync/base/report_unrecoverable_error.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/model_type_store.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/model_impl/client_tag_based_model_type_processor.h"
#include "components/sync/protocol/web_app_specifics.pb.h"
#include "url/gurl.h"
namespace web_app {
bool AreAppsLocallyInstalledByDefault() {
#if defined(OS_CHROMEOS)
// On Chrome OS, sync always locally installs an app.
return true;
#else
return false;
#endif
}
std::unique_ptr<syncer::EntityData> CreateSyncEntityData(const WebApp& app) {
auto entity_data = std::make_unique<syncer::EntityData>();
entity_data->name = app.name();
sync_pb::WebAppSpecifics* sync_data =
entity_data->specifics.mutable_web_app();
sync_data->set_launch_url(app.launch_url().spec());
sync_data->set_user_display_mode(
ToWebAppSpecificsUserDisplayMode(app.user_display_mode()));
sync_data->set_name(app.sync_data().name);
if (app.sync_data().theme_color.has_value())
sync_data->set_theme_color(app.sync_data().theme_color.value());
return entity_data;
}
void ApplySyncDataToApp(const sync_pb::WebAppSpecifics& sync_data,
WebApp* app) {
app->AddSource(Source::kSync);
// app_id is a hash of launch_url. Parse launch_url first:
GURL launch_url(sync_data.launch_url());
if (launch_url.is_empty() || !launch_url.is_valid()) {
DLOG(ERROR) << "ApplySyncDataToApp: launch_url parse error.";
return;
}
if (app->app_id() != GenerateAppIdFromURL(launch_url)) {
DLOG(ERROR) << "ApplySyncDataToApp: app_id doesn't match launch_url.";
return;
}
if (app->launch_url().is_empty()) {
app->SetLaunchUrl(std::move(launch_url));
} else if (app->launch_url() != launch_url) {
DLOG(ERROR)
<< "ApplySyncDataToApp: existing launch_url doesn't match launch_url.";
return;
}
// Always override user_display mode with a synced value.
app->SetUserDisplayMode(ToMojomDisplayMode(sync_data.user_display_mode()));
WebApp::SyncData parsed_sync_data;
parsed_sync_data.name = sync_data.name();
if (sync_data.has_theme_color())
parsed_sync_data.theme_color = sync_data.theme_color();
app->SetSyncData(std::move(parsed_sync_data));
}
WebAppSyncBridge::WebAppSyncBridge(
Profile* profile,
AbstractWebAppDatabaseFactory* database_factory,
WebAppRegistrarMutable* registrar,
SyncInstallDelegate* install_delegate)
: WebAppSyncBridge(
profile,
database_factory,
registrar,
install_delegate,
std::make_unique<syncer::ClientTagBasedModelTypeProcessor>(
syncer::WEB_APPS,
base::BindRepeating(&syncer::ReportUnrecoverableError,
chrome::GetChannel()))) {}
WebAppSyncBridge::WebAppSyncBridge(
Profile* profile,
AbstractWebAppDatabaseFactory* database_factory,
WebAppRegistrarMutable* registrar,
SyncInstallDelegate* install_delegate,
std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor)
: AppRegistryController(profile),
syncer::ModelTypeSyncBridge(std::move(change_processor)),
registrar_(registrar),
install_delegate_(install_delegate) {
DCHECK(database_factory);
DCHECK(registrar_);
database_ = std::make_unique<WebAppDatabase>(
database_factory,
base::BindRepeating(&WebAppSyncBridge::ReportErrorToChangeProcessor,
base::Unretained(this)));
}
WebAppSyncBridge::~WebAppSyncBridge() = default;
std::unique_ptr<WebAppRegistryUpdate> WebAppSyncBridge::BeginUpdate() {
DCHECK(!is_in_update_);
is_in_update_ = true;
return std::make_unique<WebAppRegistryUpdate>(
registrar_, util::PassKey<WebAppSyncBridge>());
}
void WebAppSyncBridge::CommitUpdate(
std::unique_ptr<WebAppRegistryUpdate> update,
CommitCallback callback) {
DCHECK(is_in_update_);
is_in_update_ = false;
if (update == nullptr || update->update_data().IsEmpty()) {
std::move(callback).Run(/*success*/ true);
return;
}
CheckRegistryUpdateData(update->update_data());
std::unique_ptr<RegistryUpdateData> update_data = update->TakeUpdateData();
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list =
CreateMetadataChangeList();
UpdateSync(*update_data, metadata_change_list.get());
database_->Write(
*update_data, std::move(metadata_change_list),
base::BindOnce(&WebAppSyncBridge::OnDataWritten,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
UpdateRegistrar(std::move(update_data));
}
void WebAppSyncBridge::Init(base::OnceClosure callback) {
database_->OpenDatabase(base::BindOnce(&WebAppSyncBridge::OnDatabaseOpened,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void WebAppSyncBridge::SetAppUserDisplayMode(const AppId& app_id,
DisplayMode user_display_mode) {
ScopedRegistryUpdate update(this);
WebApp* web_app = update->UpdateApp(app_id);
if (web_app)
web_app->SetUserDisplayMode(user_display_mode);
}
void WebAppSyncBridge::SetAppIsLocallyInstalledForTesting(
const AppId& app_id,
bool is_locally_installed) {
ScopedRegistryUpdate update(this);
WebApp* web_app = update->UpdateApp(app_id);
if (web_app)
web_app->SetIsLocallyInstalled(is_locally_installed);
}
WebAppSyncBridge* WebAppSyncBridge::AsWebAppSyncBridge() {
return this;
}
void WebAppSyncBridge::CheckRegistryUpdateData(
const RegistryUpdateData& update_data) const {
#if DCHECK_IS_ON()
for (const std::unique_ptr<WebApp>& web_app : update_data.apps_to_create)
DCHECK(!registrar_->GetAppById(web_app->app_id()));
for (const std::unique_ptr<WebApp>& web_app : update_data.apps_to_update)
DCHECK(registrar_->GetAppById(web_app->app_id()));
for (const AppId& app_id : update_data.apps_to_delete)
DCHECK(registrar_->GetAppById(app_id));
#endif
}
std::vector<std::unique_ptr<WebApp>> WebAppSyncBridge::UpdateRegistrar(
std::unique_ptr<RegistryUpdateData> update_data) {
registrar_->CountMutation();
std::vector<std::unique_ptr<WebApp>> apps_unregistered;
for (std::unique_ptr<WebApp>& web_app : update_data->apps_to_create) {
AppId app_id = web_app->app_id();
DCHECK(!registrar_->GetAppById(app_id));
registrar_->registry().emplace(std::move(app_id), std::move(web_app));
}
for (std::unique_ptr<WebApp>& web_app : update_data->apps_to_update) {
WebApp* original_web_app = registrar_->GetAppByIdMutable(web_app->app_id());
DCHECK(original_web_app);
// Commit previously created copy into original. Preserve original web_app
// object pointer value (the object's identity) to support stored pointers.
*original_web_app = std::move(*web_app);
}
for (const AppId& app_id : update_data->apps_to_delete) {
auto it = registrar_->registry().find(app_id);
DCHECK(it != registrar_->registry().end());
apps_unregistered.push_back(std::move(it->second));
registrar_->registry().erase(it);
}
return apps_unregistered;
}
void WebAppSyncBridge::UpdateSync(
const RegistryUpdateData& update_data,
syncer::MetadataChangeList* metadata_change_list) {
// We don't block web app subsystems on WebAppSyncBridge::MergeSyncData: we
// call WebAppProvider::OnRegistryControllerReady() right after
// change_processor()->ModelReadyToSync. As a result, subsystems may produce
// some local changes between OnRegistryControllerReady and MergeSyncData.
// Return early in this case. The processor cannot do any useful metadata
// tracking until MergeSyncData is called:
if (!change_processor()->IsTrackingMetadata())
return;
for (const std::unique_ptr<WebApp>& new_app : update_data.apps_to_create) {
if (new_app->IsSynced()) {
change_processor()->Put(new_app->app_id(), CreateSyncEntityData(*new_app),
metadata_change_list);
}
}
for (const std::unique_ptr<WebApp>& new_state : update_data.apps_to_update) {
const AppId& app_id = new_state->app_id();
// Find the current state of the app to be overritten.
const WebApp* current_state = registrar_->GetAppById(app_id);
DCHECK(current_state);
// Include the app in the sync "view" if IsSynced flag becomes true. Update
// the app if IsSynced flag stays true. Exclude the app from the sync "view"
// if IsSynced flag becomes false.
//
// TODO(loyso): Send an update to sync server only if any sync-specific
// data was changed. Implement some dirty flags in WebApp setter methods.
if (new_state->IsSynced()) {
change_processor()->Put(app_id, CreateSyncEntityData(*new_state),
metadata_change_list);
} else if (current_state->IsSynced()) {
change_processor()->Delete(app_id, metadata_change_list);
}
}
for (const AppId& app_id_to_delete : update_data.apps_to_delete) {
const WebApp* current_state = registrar_->GetAppById(app_id_to_delete);
DCHECK(current_state);
// Exclude the app from the sync "view" if IsSynced flag was true.
if (current_state->IsSynced())
change_processor()->Delete(app_id_to_delete, metadata_change_list);
}
}
void WebAppSyncBridge::OnDatabaseOpened(
base::OnceClosure callback,
Registry registry,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
// Provide sync metadata to the processor _before_ any local changes occur.
change_processor()->ModelReadyToSync(std::move(metadata_batch));
registrar_->InitRegistry(std::move(registry));
std::move(callback).Run();
MaybeInstallAppsInSyncInstall();
}
void WebAppSyncBridge::OnDataWritten(CommitCallback callback, bool success) {
if (!success)
DLOG(ERROR) << "WebAppSyncBridge commit failed";
std::move(callback).Run(success);
}
void WebAppSyncBridge::ReportErrorToChangeProcessor(
const syncer::ModelError& error) {
change_processor()->ReportError(error);
}
void WebAppSyncBridge::MergeLocalAppsToSync(
const syncer::EntityChangeList& entity_data,
syncer::MetadataChangeList* metadata_change_list) {
// Build a helper set of the sync server apps to speed up lookups. The
// flat_set will reuse the underlying memory of this vector. app_id is storage
// key.
std::vector<AppId> storage_keys;
storage_keys.reserve(entity_data.size());
for (const auto& change : entity_data)
storage_keys.push_back(change->storage_key());
// Sort only once.
base::flat_set<AppId> sync_server_apps(std::move(storage_keys));
for (const WebApp& app : registrar_->AllApps()) {
if (!app.IsSynced())
continue;
bool exists_remotely = sync_server_apps.contains(app.app_id());
if (!exists_remotely) {
change_processor()->Put(app.app_id(), CreateSyncEntityData(app),
metadata_change_list);
}
}
}
void WebAppSyncBridge::ApplySyncDataChange(
const syncer::EntityChange& change,
RegistryUpdateData* update_local_data) {
// app_id is storage key.
const AppId& app_id = change.storage_key();
const WebApp* existing_web_app = registrar_->GetAppByIdMutable(app_id);
// Handle deletion first.
if (change.type() == syncer::EntityChange::ACTION_DELETE) {
if (!existing_web_app) {
DLOG(ERROR) << "ApplySyncDataChange error: no app to delete";
return;
}
// Do copy on write:
auto app_copy = std::make_unique<WebApp>(*existing_web_app);
app_copy->RemoveSource(Source::kSync);
if (app_copy->HasAnySources())
update_local_data->apps_to_update.push_back(std::move(app_copy));
else
update_local_data->apps_to_delete.push_back(app_id);
return;
}
// Handle EntityChange::ACTION_ADD and EntityChange::ACTION_UPDATE.
DCHECK(change.data().specifics.has_web_app());
const sync_pb::WebAppSpecifics& specifics = change.data().specifics.web_app();
if (existing_web_app) {
// Any entities that appear in both sets must be merged.
// Do copy on write:
auto app_copy = std::make_unique<WebApp>(*existing_web_app);
ApplySyncDataToApp(specifics, app_copy.get());
// Preserve web_app->is_locally_installed user's choice here.
update_local_data->apps_to_update.push_back(std::move(app_copy));
} else {
// Any remote entities that don’t exist locally must be written to local
// storage.
auto web_app = std::make_unique<WebApp>(app_id);
// Request a followup sync-initiated install for this stub app to fetch
// full local data and all the icons.
web_app->SetIsInSyncInstall(true);
ApplySyncDataToApp(specifics, web_app.get());
// For a new app, automatically choose if we want to install it locally.
web_app->SetIsLocallyInstalled(AreAppsLocallyInstalledByDefault());
update_local_data->apps_to_create.push_back(std::move(web_app));
}
}
void WebAppSyncBridge::ApplySyncChangesToRegistrar(
std::unique_ptr<RegistryUpdateData> update_local_data) {
if (update_local_data->IsEmpty())
return;
std::vector<WebApp*> apps_to_install;
for (const auto& web_app : update_local_data->apps_to_create)
apps_to_install.push_back(web_app.get());
std::vector<std::unique_ptr<WebApp>> apps_unregistered =
UpdateRegistrar(std::move(update_local_data));
// Do a full follow up install for all remote entities that don’t exist
// locally.
if (!apps_to_install.empty()) {
install_delegate_->InstallWebAppsAfterSync(std::move(apps_to_install),
base::DoNothing());
}
// Do a full follow up uninstall for all deleted remote entities that exist
// locally and not needed by other sources. We need to clean up disk data
// (icons).
if (!apps_unregistered.empty()) {
install_delegate_->UninstallWebAppsAfterSync(std::move(apps_unregistered),
base::DoNothing());
}
}
std::unique_ptr<syncer::MetadataChangeList>
WebAppSyncBridge::CreateMetadataChangeList() {
return syncer::ModelTypeStore::WriteBatch::CreateMetadataChangeList();
}
base::Optional<syncer::ModelError> WebAppSyncBridge::MergeSyncData(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_data) {
CHECK(change_processor()->IsTrackingMetadata());
auto update_local_data = std::make_unique<RegistryUpdateData>();
for (const auto& change : entity_data) {
DCHECK_NE(change->type(), syncer::EntityChange::ACTION_DELETE);
ApplySyncDataChange(*change, update_local_data.get());
}
MergeLocalAppsToSync(entity_data, metadata_change_list.get());
database_->Write(*update_local_data, std::move(metadata_change_list),
base::DoNothing());
ApplySyncChangesToRegistrar(std::move(update_local_data));
return base::nullopt;
}
base::Optional<syncer::ModelError> WebAppSyncBridge::ApplySyncChanges(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
CHECK(change_processor()->IsTrackingMetadata());
auto update_local_data = std::make_unique<RegistryUpdateData>();
for (const auto& change : entity_changes)
ApplySyncDataChange(*change, update_local_data.get());
database_->Write(*update_local_data, std::move(metadata_change_list),
base::DoNothing());
ApplySyncChangesToRegistrar(std::move(update_local_data));
return base::nullopt;
}
void WebAppSyncBridge::GetData(StorageKeyList storage_keys,
DataCallback callback) {
auto data_batch = std::make_unique<syncer::MutableDataBatch>();
for (const AppId& app_id : storage_keys) {
const WebApp* app = registrar_->GetAppById(app_id);
if (app && app->IsSynced())
data_batch->Put(app->app_id(), CreateSyncEntityData(*app));
}
std::move(callback).Run(std::move(data_batch));
}
void WebAppSyncBridge::GetAllDataForDebugging(DataCallback callback) {
auto data_batch = std::make_unique<syncer::MutableDataBatch>();
for (const WebApp& app : registrar_->AllApps()) {
if (app.IsSynced())
data_batch->Put(app.app_id(), CreateSyncEntityData(app));
}
std::move(callback).Run(std::move(data_batch));
}
std::string WebAppSyncBridge::GetClientTag(
const syncer::EntityData& entity_data) {
DCHECK(entity_data.specifics.has_web_app());
const GURL launch_url(entity_data.specifics.web_app().launch_url());
DCHECK(!launch_url.is_empty());
DCHECK(launch_url.is_valid());
return GenerateAppIdFromURL(launch_url);
}
std::string WebAppSyncBridge::GetStorageKey(
const syncer::EntityData& entity_data) {
return GetClientTag(entity_data);
}
void WebAppSyncBridge::MaybeInstallAppsInSyncInstall() {
std::vector<WebApp*> apps_in_sync_install;
for (WebApp& app : registrar_->AllAppsMutable()) {
if (app.is_in_sync_install())
apps_in_sync_install.push_back(&app);
}
if (!apps_in_sync_install.empty()) {
install_delegate_->InstallWebAppsAfterSync(std::move(apps_in_sync_install),
base::DoNothing());
}
}
} // namespace web_app