blob: 1379e23fe6d9e7b829c4c8d7059adeaeac02db6e [file] [log] [blame]
// Copyright 2021 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 "components/desks_storage/core/desk_sync_bridge.h"
#include <algorithm>
#include "ash/public/cpp/desk_template.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check_op.h"
#include "base/guid.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ui/base/window_state_type.h"
#include "components/account_id/account_id.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/window_info.h"
#include "components/desks_storage/core/desk_model_observer.h"
#include "components/desks_storage/core/desk_template_conversion.h"
#include "components/desks_storage/core/desk_template_util.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/model_type_change_processor.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/model_type_state.pb.h"
#include "components/sync/protocol/workspace_desk_specifics.pb.h"
#include "extensions/common/constants.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/ui_base_types.h"
namespace desks_storage {
using BrowserAppTab =
sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_BrowserAppTab;
using BrowserAppWindow = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow;
using ash::DeskTemplate;
using ash::DeskTemplateSource;
using WindowState = sync_pb::WorkspaceDeskSpecifics_WindowState;
using WindowBound = sync_pb::WorkspaceDeskSpecifics_WindowBound;
using ProgressiveWebApp = sync_pb::WorkspaceDeskSpecifics_ProgressiveWebApp;
using ChromeApp = sync_pb::WorkspaceDeskSpecifics_ChromeApp;
using WorkspaceDeskSpecifics_App = sync_pb::WorkspaceDeskSpecifics_App;
namespace {
using syncer::ModelTypeStore;
// The maximum number of templates the local storage can hold.
constexpr std::size_t kMaxTemplateCount = 6u;
// The maximum number of bytes a template can be.
// Sync server silently ignores large items. The client-side
// needs to check item size to avoid sending large items.
// This limit follows precedent set by the chrome extension API:
// chrome.storage.sync.QUOTA_BYTES_PER_ITEM.
constexpr std::size_t kMaxTemplateSize = 8192u;
// Allocate a EntityData and copies |specifics| into it.
std::unique_ptr<syncer::EntityData> CopyToEntityData(
const sync_pb::WorkspaceDeskSpecifics& specifics) {
auto entity_data = std::make_unique<syncer::EntityData>();
*entity_data->specifics.mutable_workspace_desk() = specifics;
entity_data->name = specifics.uuid();
entity_data->creation_time = desk_template_conversion::ProtoTimeToTime(
specifics.created_time_windows_epoch_micros());
return entity_data;
}
// Parses the content of |record_list| into |*desk_templates|.
absl::optional<syncer::ModelError> ParseDeskTemplatesOnBackendSequence(
std::map<base::GUID, std::unique_ptr<DeskTemplate>>* desk_templates,
std::unique_ptr<ModelTypeStore::RecordList> record_list) {
DCHECK(desk_templates);
DCHECK(desk_templates->empty());
DCHECK(record_list);
for (const syncer::ModelTypeStore::Record& r : *record_list) {
auto specifics = std::make_unique<sync_pb::WorkspaceDeskSpecifics>();
if (specifics->ParseFromString(r.value)) {
const base::GUID uuid =
base::GUID::ParseCaseInsensitive(specifics->uuid());
if (!uuid.is_valid()) {
return syncer::ModelError(
FROM_HERE,
base::StringPrintf("Failed to parse WorkspaceDeskSpecifics uuid %s",
specifics->uuid().c_str()));
}
std::unique_ptr<ash::DeskTemplate> entry =
DeskSyncBridge::FromSyncProto(*specifics);
if (!entry)
continue;
(*desk_templates)[uuid] = std::move(entry);
} else {
return syncer::ModelError(
FROM_HERE, "Failed to deserialize WorkspaceDeskSpecifics.");
}
}
return absl::nullopt;
}
// Fill |out_gurls| using tabs' URL in |browser_app_window|.
void FillUrlList(std::vector<GURL>* out_gurls,
const BrowserAppWindow& browser_app_window) {
for (auto tab : browser_app_window.tabs()) {
if (tab.has_url())
out_gurls->emplace_back(tab.url());
}
}
// Get App ID from App proto.
std::string GetAppId(const sync_pb::WorkspaceDeskSpecifics_App& app) {
switch (app.app().app_case()) {
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::APP_NOT_SET:
// Return an empty string to indicate this app is unsupported.
return std::string();
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kBrowserAppWindow:
// Browser app has a known app ID.
return std::string(extension_misc::kChromeAppId);
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kChromeApp:
return app.app().chrome_app().app_id();
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kProgressWebApp:
return app.app().progress_web_app().app_id();
// Leave out the default case to let compiler to ensure we have
// exhaustively handled all cases.
}
}
// Convert App proto to |app_restore::AppLaunchInfo|.
std::unique_ptr<app_restore::AppLaunchInfo> ConvertToAppLaunchInfo(
const sync_pb::WorkspaceDeskSpecifics_App& app) {
const int32_t window_id = app.window_id();
const std::string app_id = GetAppId(app);
if (app_id.empty())
return nullptr;
std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
std::make_unique<app_restore::AppLaunchInfo>(app_id, window_id);
if (app.has_display_id())
app_launch_info->display_id = app.display_id();
switch (app.app().app_case()) {
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::APP_NOT_SET:
// This should never happen. |APP_NOT_SET| corresponds to empty |app_id|.
// This method will early return when |app_id| is empty.
NOTREACHED();
break;
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kBrowserAppWindow:
if (app.app().browser_app_window().has_active_tab_index()) {
app_launch_info->active_tab_index =
app.app().browser_app_window().active_tab_index();
}
app_launch_info->urls.emplace();
FillUrlList(&app_launch_info->urls.value(),
app.app().browser_app_window());
break;
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kChromeApp:
// |app_id| is enough to identify a Chrome app.
break;
case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kProgressWebApp:
// |app_id| is enough to identify a Progressive Web app.
break;
// Leave out the default case to let compiler to ensure we have
// exhaustively handled all cases.
}
return app_launch_info;
}
// Convert Sync proto WindowState |state| to ui::WindowShowState used by
// the app_restore::WindowInfo struct.
ui::WindowShowState ToUiWindowState(WindowState state) {
switch (state) {
case WindowState::WorkspaceDeskSpecifics_WindowState_UNKNOWN_WINDOW_STATE:
return ui::WindowShowState::SHOW_STATE_NORMAL;
case WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL:
return ui::WindowShowState::SHOW_STATE_NORMAL;
case WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED:
return ui::WindowShowState::SHOW_STATE_MINIMIZED;
case WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED:
return ui::WindowShowState::SHOW_STATE_MAXIMIZED;
case WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN:
return ui::WindowShowState::SHOW_STATE_FULLSCREEN;
case WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED:
return ui::WindowShowState::SHOW_STATE_NORMAL;
case WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED:
return ui::WindowShowState::SHOW_STATE_NORMAL;
// Leave out the default case to let compiler to ensure we have
// exhaustively handled all cases.
}
}
// Convert Sync proto WindowState |state| to chromeos::WindowStateType used by
// the app_restore::WindowInfo struct.
chromeos::WindowStateType ToChromeOsWindowState(WindowState state) {
switch (state) {
case WindowState::WorkspaceDeskSpecifics_WindowState_UNKNOWN_WINDOW_STATE:
return chromeos::WindowStateType::kNormal;
case WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL:
return chromeos::WindowStateType::kNormal;
case WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED:
return chromeos::WindowStateType::kMinimized;
case WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED:
return chromeos::WindowStateType::kMaximized;
case WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN:
return chromeos::WindowStateType::kFullscreen;
case WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED:
return chromeos::WindowStateType::kPrimarySnapped;
case WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED:
return chromeos::WindowStateType::kSecondarySnapped;
// Leave out the default case to let compiler to ensure we have
// exhaustively handled all cases.
}
}
// Convert chromeos::WindowStateType to Sync proto WindowState.
WindowState FromChromeOsWindowState(chromeos::WindowStateType state) {
switch (state) {
case chromeos::WindowStateType::kDefault:
case chromeos::WindowStateType::kNormal:
case chromeos::WindowStateType::kInactive:
case chromeos::WindowStateType::kAutoPositioned:
case chromeos::WindowStateType::kPinned:
case chromeos::WindowStateType::kTrustedPinned:
case chromeos::WindowStateType::kPip:
return WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL;
case chromeos::WindowStateType::kMinimized:
return WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED;
case chromeos::WindowStateType::kMaximized:
return WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED;
case chromeos::WindowStateType::kFullscreen:
return WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN;
case chromeos::WindowStateType::kPrimarySnapped:
return WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED;
case chromeos::WindowStateType::kSecondarySnapped:
return WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED;
// Leave out the default case to let compiler to ensure we have
// exhaustively handled all cases.
}
}
// Convert ui::WindowShowState to Sync proto WindowState.
WindowState FromUiWindowState(ui::WindowShowState state) {
switch (state) {
case ui::WindowShowState::SHOW_STATE_DEFAULT:
case ui::WindowShowState::SHOW_STATE_NORMAL:
case ui::WindowShowState::SHOW_STATE_INACTIVE:
case ui::WindowShowState::SHOW_STATE_END:
return WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL;
case ui::WindowShowState::SHOW_STATE_MINIMIZED:
return WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED;
case ui::WindowShowState::SHOW_STATE_MAXIMIZED:
return WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED;
case ui::WindowShowState::SHOW_STATE_FULLSCREEN:
return WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN;
// Leave out the default case to let compiler to ensure we have
// exhaustively handled all cases.
}
}
// Fill |out_browser_app_window| with the given GURLs as BrowserAppTabs.
void FillBrowserAppTabs(BrowserAppWindow* out_browser_app_window,
const std::vector<GURL>& gurls) {
for (const auto& gurl : gurls) {
const std::string& url = gurl.spec();
if (url.empty()) {
// Skip invalid URLs.
continue;
}
BrowserAppTab* browser_app_tab = out_browser_app_window->add_tabs();
browser_app_tab->set_url(url);
}
}
// Fill |out_browser_app_window| with urls and tab information from
// |app_restore_data|.
void FillBrowserAppWindow(BrowserAppWindow* out_browser_app_window,
const app_restore::AppRestoreData* app_restore_data) {
if (app_restore_data->urls.has_value())
FillBrowserAppTabs(out_browser_app_window, app_restore_data->urls.value());
if (app_restore_data->active_tab_index.has_value()) {
out_browser_app_window->set_active_tab_index(
app_restore_data->active_tab_index.value());
}
}
// Fill |out_window_bound| with information from |bound|.
void FillWindowBound(WindowBound* out_window_bound, const gfx::Rect& bound) {
out_window_bound->set_left(bound.x());
out_window_bound->set_top(bound.y());
out_window_bound->set_width(bound.width());
out_window_bound->set_height(bound.height());
}
// Fill |out_app| with information from |window_info|.
void FillAppWithWindowInfo(WorkspaceDeskSpecifics_App* out_app,
const app_restore::WindowInfo* window_info) {
if (window_info->activation_index.has_value())
out_app->set_z_index(window_info->activation_index.value());
if (window_info->current_bounds.has_value()) {
FillWindowBound(out_app->mutable_window_bound(),
window_info->current_bounds.value());
}
if (window_info->window_state_type.has_value()) {
out_app->set_window_state(
FromChromeOsWindowState(window_info->window_state_type.value()));
}
if (window_info->pre_minimized_show_state_type.has_value()) {
out_app->set_pre_minimized_window_state(
FromUiWindowState(window_info->pre_minimized_show_state_type.value()));
}
// AppRestoreData.GetWindowInfo does not include |display_id| in the returned
// WindowInfo. Therefore, we are not filling |display_id| here.
}
// Fill |out_app| with |display_id| from |app_restore_data|.
void FillAppWithDisplayId(WorkspaceDeskSpecifics_App* out_app,
const app_restore::AppRestoreData* app_restore_data) {
if (app_restore_data->display_id.has_value())
out_app->set_display_id(app_restore_data->display_id.value());
}
// Fill |out_app| with |app_restore_data|.
void FillApp(WorkspaceDeskSpecifics_App* out_app,
const std::string& app_id,
const apps::mojom::AppType app_type,
const app_restore::AppRestoreData* app_restore_data) {
FillAppWithWindowInfo(out_app, app_restore_data->GetWindowInfo().get());
// AppRestoreData.GetWindowInfo does not include |display_id| in the returned
// WindowInfo. We need to fill the |display_id| from AppRestoreData.
FillAppWithDisplayId(out_app, app_restore_data);
// See definition components/services/app_service/public/mojom/types.mojom
switch (app_type) {
case apps::mojom::AppType::kWeb: {
if (extension_misc::kChromeAppId == app_id) {
// Chrome Browser Window.
BrowserAppWindow* browser_app_window =
out_app->mutable_app()->mutable_browser_app_window();
FillBrowserAppWindow(browser_app_window, app_restore_data);
} else {
// PWA app.
ProgressiveWebApp* pwa_window =
out_app->mutable_app()->mutable_progress_web_app();
pwa_window->set_app_id(app_id);
if (app_restore_data->title.has_value()) {
pwa_window->set_title(
base::UTF16ToUTF8(app_restore_data->title.value()));
}
}
break;
}
case apps::mojom::AppType::kStandaloneBrowser: {
// Lacros Browser App. This is currently unsupported.
// Note, Lacros-chrome has app ID kLacrosAppId, that is different than
// kChromeAppId.
break;
}
case apps::mojom::AppType::kChromeApp: {
// Chrome extension backed app, Chrome Apps
ChromeApp* chrome_app_window =
out_app->mutable_app()->mutable_chrome_app();
chrome_app_window->set_app_id(app_id);
if (app_restore_data->title.has_value()) {
chrome_app_window->set_title(
base::UTF16ToUTF8(app_restore_data->title.value()));
}
break;
}
default: {
// Unhandled app type.
break;
}
}
}
// Fill |out_window_info| with information from Sync proto |app|.
void FillWindowInfoFromProto(app_restore::WindowInfo* out_window_info,
sync_pb::WorkspaceDeskSpecifics_App& app) {
if (app.has_window_state() &&
sync_pb::WorkspaceDeskSpecifics_WindowState_IsValid(app.window_state())) {
out_window_info->window_state_type.emplace(
ToChromeOsWindowState(app.window_state()));
}
if (app.has_window_bound()) {
out_window_info->current_bounds.emplace(
app.window_bound().left(), app.window_bound().top(),
app.window_bound().width(), app.window_bound().height());
}
if (app.has_z_index())
out_window_info->activation_index.emplace(app.z_index());
if (app.has_display_id())
out_window_info->display_id.emplace(app.display_id());
if (app.has_pre_minimized_window_state() &&
sync_pb::WorkspaceDeskSpecifics_WindowState_IsValid(app.window_state())) {
out_window_info->pre_minimized_show_state_type.emplace(
ToUiWindowState(app.pre_minimized_window_state()));
}
}
// Convert a desk template to |app_restore::RestoreData|.
std::unique_ptr<app_restore::RestoreData> ConvertToRestoreData(
const sync_pb::WorkspaceDeskSpecifics& entry_proto) {
std::unique_ptr<app_restore::RestoreData> restore_data =
std::make_unique<app_restore::RestoreData>();
for (auto app_proto : entry_proto.desk().apps()) {
std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
ConvertToAppLaunchInfo(app_proto);
if (!app_launch_info) {
// Skip unsupported app.
continue;
}
const std::string app_id = app_launch_info->app_id;
restore_data->AddAppLaunchInfo(std::move(app_launch_info));
app_restore::WindowInfo app_window_info;
FillWindowInfoFromProto(&app_window_info, app_proto);
restore_data->ModifyWindowInfo(app_id, app_proto.window_id(),
app_window_info);
}
return restore_data;
}
// Fill a desk template |out_entry_proto| with information from
// |restore_data|.
void FillWorkspaceDeskSpecifics(
sync_pb::WorkspaceDeskSpecifics* out_entry_proto,
apps::AppRegistryCache* apps_cache,
const app_restore::RestoreData* restore_data) {
DCHECK(apps_cache);
for (auto const& app_id_to_launch_list :
restore_data->app_id_to_launch_list()) {
const std::string app_id = app_id_to_launch_list.first;
for (auto const& window_id_to_launch_info : app_id_to_launch_list.second) {
const int window_id = window_id_to_launch_info.first;
const app_restore::AppRestoreData* app_restore_data =
window_id_to_launch_info.second.get();
// The apps cache returns kChromeApp for browser windows, therefore we
// short circuit the cache retrieval if we get the browser ID.
const apps::mojom::AppType app_type =
app_id == extension_misc::kChromeAppId
? apps::mojom::AppType::kWeb
: apps_cache->GetAppType(app_id);
WorkspaceDeskSpecifics_App* app =
out_entry_proto->mutable_desk()->add_apps();
app->set_window_id(window_id);
FillApp(app, app_id, app_type, app_restore_data);
}
}
}
} // namespace
DeskSyncBridge::DeskSyncBridge(
std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor,
syncer::OnceModelTypeStoreFactory create_store_callback,
const AccountId& account_id)
: ModelTypeSyncBridge(std::move(change_processor)),
is_ready_(false),
account_id_(account_id) {
std::move(create_store_callback)
.Run(syncer::WORKSPACE_DESK,
base::BindOnce(&DeskSyncBridge::OnStoreCreated,
weak_ptr_factory_.GetWeakPtr()));
}
DeskSyncBridge::~DeskSyncBridge() = default;
std::unique_ptr<DeskTemplate> DeskSyncBridge::FromSyncProto(
const sync_pb::WorkspaceDeskSpecifics& pb_entry) {
const std::string uuid(pb_entry.uuid());
if (uuid.empty() || !base::GUID::ParseCaseInsensitive(uuid).is_valid())
return nullptr;
const base::Time created_time = desk_template_conversion::ProtoTimeToTime(
pb_entry.created_time_windows_epoch_micros());
// Protobuf parsing enforces UTF-8 encoding for all strings.
std::unique_ptr<DeskTemplate> desk_template = std::make_unique<DeskTemplate>(
uuid, ash::DeskTemplateSource::kUser, pb_entry.name(), created_time);
if (pb_entry.has_updated_time_windows_epoch_micros()) {
desk_template->set_updated_time(desk_template_conversion::ProtoTimeToTime(
pb_entry.updated_time_windows_epoch_micros()));
}
desk_template->set_desk_restore_data(ConvertToRestoreData(pb_entry));
return desk_template;
}
std::unique_ptr<syncer::MetadataChangeList>
DeskSyncBridge::CreateMetadataChangeList() {
return ModelTypeStore::WriteBatch::CreateMetadataChangeList();
}
absl::optional<syncer::ModelError> DeskSyncBridge::MergeSyncData(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_data) {
// MergeSyncData will be called when Desk Template model type is enabled to
// start syncing. There could be local desk templates that user has created
// before enabling sync or during the time when Desk Template sync is
// disabled. We should merge local and server data. We will send all
// local-only templates to server and save server templates to local.
UploadLocalOnlyData(metadata_change_list.get(), entity_data);
// Apply server changes locally. Currently, if a template exists on both
// local and server side, the server version will win.
// TODO(yzd) We will add a template update timestamp and update this logic to
// be: for templates that exist on both local and server side, we will keep
// the one with later update timestamp.
return ApplySyncChanges(std::move(metadata_change_list),
std::move(entity_data));
}
absl::optional<syncer::ModelError> DeskSyncBridge::ApplySyncChanges(
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
syncer::EntityChangeList entity_changes) {
std::vector<const DeskTemplate*> added_or_updated;
std::vector<std::string> removed;
std::unique_ptr<ModelTypeStore::WriteBatch> batch =
store_->CreateWriteBatch();
for (const std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
const base::GUID uuid =
base::GUID::ParseCaseInsensitive(change->storage_key());
if (!uuid.is_valid()) {
// Skip invalid storage keys.
continue;
}
switch (change->type()) {
case syncer::EntityChange::ACTION_DELETE: {
if (entries_.find(uuid) != entries_.end()) {
entries_.erase(uuid);
batch->DeleteData(uuid.AsLowercaseString());
removed.push_back(uuid.AsLowercaseString());
}
break;
}
case syncer::EntityChange::ACTION_UPDATE:
case syncer::EntityChange::ACTION_ADD: {
const sync_pb::WorkspaceDeskSpecifics& specifics =
change->data().specifics.workspace_desk();
std::unique_ptr<DeskTemplate> remote_entry =
DeskSyncBridge::FromSyncProto(specifics);
if (!remote_entry) {
// Skip invalid entries.
continue;
}
DCHECK_EQ(uuid, remote_entry->uuid());
std::string serialized_remote_entry = specifics.SerializeAsString();
// Add/update the remote_entry to the model.
entries_[uuid] = std::move(remote_entry);
added_or_updated.push_back(GetUserEntryByUUID(uuid));
// Write to the store.
batch->WriteData(uuid.AsLowercaseString(), serialized_remote_entry);
break;
}
}
}
batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
Commit(std::move(batch));
NotifyRemoteDeskTemplateAddedOrUpdated(added_or_updated);
NotifyRemoteDeskTemplateDeleted(removed);
return absl::nullopt;
}
void DeskSyncBridge::GetData(StorageKeyList storage_keys,
DataCallback callback) {
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const std::string& uuid : storage_keys) {
const DeskTemplate* entry =
GetUserEntryByUUID(base::GUID::ParseCaseInsensitive(uuid));
if (!entry) {
continue;
}
batch->Put(uuid, CopyToEntityData(ToSyncProto(entry)));
}
std::move(callback).Run(std::move(batch));
}
void DeskSyncBridge::GetAllDataForDebugging(DataCallback callback) {
auto batch = std::make_unique<syncer::MutableDataBatch>();
for (const auto& it : entries_) {
batch->Put(it.first.AsLowercaseString(),
CopyToEntityData(ToSyncProto(it.second.get())));
}
std::move(callback).Run(std::move(batch));
}
std::string DeskSyncBridge::GetClientTag(
const syncer::EntityData& entity_data) {
return GetStorageKey(entity_data);
}
std::string DeskSyncBridge::GetStorageKey(
const syncer::EntityData& entity_data) {
return entity_data.specifics.workspace_desk().uuid();
}
void DeskSyncBridge::GetAllEntries(GetAllEntriesCallback callback) {
std::vector<DeskTemplate*> entries;
if (!IsReady()) {
std::move(callback).Run(GetAllEntriesStatus::kFailure, std::move(entries));
return;
}
for (const auto& it : policy_entries_)
entries.push_back(it.get());
for (const auto& it : entries_) {
DCHECK_EQ(it.first, it.second->uuid());
entries.push_back(it.second.get());
}
std::move(callback).Run(GetAllEntriesStatus::kOk, std::move(entries));
}
void DeskSyncBridge::GetEntryByUUID(const std::string& uuid_str,
GetEntryByUuidCallback callback) {
if (!IsReady()) {
std::move(callback).Run(GetEntryByUuidStatus::kFailure, nullptr);
return;
}
const base::GUID uuid = base::GUID::ParseCaseInsensitive(uuid_str);
if (!uuid.is_valid()) {
std::move(callback).Run(GetEntryByUuidStatus::kInvalidUuid, nullptr);
return;
}
auto it = entries_.find(uuid);
if (it == entries_.end()) {
std::unique_ptr<DeskTemplate> policy_entry =
GetAdminDeskTemplateByUUID(uuid_str);
if (policy_entry) {
std::move(callback).Run(GetEntryByUuidStatus::kOk,
std::move(policy_entry));
} else {
std::move(callback).Run(GetEntryByUuidStatus::kNotFound, nullptr);
}
} else {
std::move(callback).Run(GetEntryByUuidStatus::kOk,
it->second.get()->Clone());
}
}
void DeskSyncBridge::AddOrUpdateEntry(std::unique_ptr<DeskTemplate> new_entry,
AddOrUpdateEntryCallback callback) {
if (!IsReady()) {
// This sync bridge has not finished initializing. Do not save the new entry
// yet.
std::move(callback).Run(AddOrUpdateEntryStatus::kFailure);
return;
}
base::GUID uuid = new_entry->uuid();
if (!uuid.is_valid()) {
std::move(callback).Run(AddOrUpdateEntryStatus::kInvalidArgument);
return;
}
// When a user creates a desk template locally, the desk template has |kUser|
// as its source. Only user desk templates should be saved to Sync.
DCHECK_EQ(DeskTemplateSource::kUser, new_entry->source());
auto entry = new_entry->Clone();
entry->set_template_name(
base::CollapseWhitespace(new_entry->template_name(), true));
// While we still find duplicate names iterate the duplicate number. i.e.
// if there are 4 duplicates of some template name then this iterates until
// the current template will be named 5.
while (HasUserTemplateWithName(entry->template_name())) {
entry->set_template_name(
desk_template_util::AppendDuplicateNumberToDuplicateName(
entry->template_name()));
}
std::unique_ptr<ModelTypeStore::WriteBatch> batch =
store_->CreateWriteBatch();
// Check the new entry size and ensure it is below the size limit.
auto sync_proto = ToSyncProto(entry.get());
if (sync_proto.ByteSizeLong() > kMaxTemplateSize) {
std::move(callback).Run(AddOrUpdateEntryStatus::kEntryTooLarge);
return;
}
// Add/update this entry to the store and model.
auto entity_data = CopyToEntityData(sync_proto);
change_processor()->Put(uuid.AsLowercaseString(), std::move(entity_data),
batch->GetMetadataChangeList());
entries_[uuid] = std::move(entry);
const DeskTemplate* result = GetUserEntryByUUID(uuid);
batch->WriteData(uuid.AsLowercaseString(),
ToSyncProto(result).SerializeAsString());
Commit(std::move(batch));
std::move(callback).Run(AddOrUpdateEntryStatus::kOk);
}
void DeskSyncBridge::DeleteEntry(const std::string& uuid_str,
DeleteEntryCallback callback) {
if (!IsReady()) {
// This sync bridge has not finished initializing.
// Cannot delete anything.
std::move(callback).Run(DeleteEntryStatus::kFailure);
return;
}
const base::GUID uuid = base::GUID::ParseCaseInsensitive(uuid_str);
if (GetUserEntryByUUID(uuid) == nullptr) {
// Consider the deletion successful if the entry does not exist.
std::move(callback).Run(DeleteEntryStatus::kOk);
return;
}
std::unique_ptr<ModelTypeStore::WriteBatch> batch =
store_->CreateWriteBatch();
change_processor()->Delete(uuid.AsLowercaseString(),
batch->GetMetadataChangeList());
entries_.erase(uuid);
batch->DeleteData(uuid.AsLowercaseString());
Commit(std::move(batch));
std::move(callback).Run(DeleteEntryStatus::kOk);
}
void DeskSyncBridge::DeleteAllEntries(DeleteEntryCallback callback) {
if (!IsReady()) {
// This sync bridge has not finished initializing.
// Cannot delete anything.
std::move(callback).Run(DeleteEntryStatus::kFailure);
return;
}
std::unique_ptr<ModelTypeStore::WriteBatch> batch =
store_->CreateWriteBatch();
std::vector<base::GUID> all_uuids = GetAllEntryUuids();
for (const auto& uuid : all_uuids) {
change_processor()->Delete(uuid.AsLowercaseString(),
batch->GetMetadataChangeList());
batch->DeleteData(uuid.AsLowercaseString());
}
entries_.clear();
std::move(callback).Run(DeleteEntryStatus::kOk);
}
std::size_t DeskSyncBridge::GetEntryCount() const {
return entries_.size() + policy_entries_.size();
}
std::size_t DeskSyncBridge::GetMaxEntryCount() const {
return kMaxTemplateCount + policy_entries_.size();
}
std::vector<base::GUID> DeskSyncBridge::GetAllEntryUuids() const {
std::vector<base::GUID> keys;
for (const auto& it : policy_entries_)
keys.push_back(it.get()->uuid());
for (const auto& it : entries_) {
DCHECK_EQ(it.first, it.second->uuid());
keys.emplace_back(it.first);
}
return keys;
}
bool DeskSyncBridge::IsReady() const {
if (is_ready_) {
DCHECK(store_);
}
return is_ready_;
}
bool DeskSyncBridge::IsSyncing() const {
return change_processor()->IsTrackingMetadata();
}
sync_pb::WorkspaceDeskSpecifics DeskSyncBridge::ToSyncProto(
const DeskTemplate* desk_template) {
apps::AppRegistryCache* cache =
apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id_);
DCHECK(cache);
sync_pb::WorkspaceDeskSpecifics pb_entry;
pb_entry.set_uuid(desk_template->uuid().AsLowercaseString());
pb_entry.set_name(base::UTF16ToUTF8(desk_template->template_name()));
pb_entry.set_created_time_windows_epoch_micros(
desk_template_conversion::TimeToProtoTime(desk_template->created_time()));
if (desk_template->WasUpdatedSinceCreation()) {
pb_entry.set_updated_time_windows_epoch_micros(
desk_template_conversion::TimeToProtoTime(
desk_template->GetLastUpdatedTime()));
}
if (desk_template->desk_restore_data()) {
FillWorkspaceDeskSpecifics(&pb_entry, cache,
desk_template->desk_restore_data());
}
return pb_entry;
}
const DeskTemplate* DeskSyncBridge::GetUserEntryByUUID(
const base::GUID& uuid) const {
auto it = entries_.find(uuid);
if (it == entries_.end())
return nullptr;
return it->second.get();
}
void DeskSyncBridge::NotifyDeskModelLoaded() {
for (DeskModelObserver& observer : observers_) {
observer.DeskModelLoaded();
}
}
void DeskSyncBridge::NotifyRemoteDeskTemplateAddedOrUpdated(
const std::vector<const DeskTemplate*>& new_entries) {
if (new_entries.empty()) {
return;
}
for (DeskModelObserver& observer : observers_) {
observer.EntriesAddedOrUpdatedRemotely(new_entries);
}
}
void DeskSyncBridge::NotifyRemoteDeskTemplateDeleted(
const std::vector<std::string>& uuids) {
if (uuids.empty()) {
return;
}
for (DeskModelObserver& observer : observers_) {
observer.EntriesRemovedRemotely(uuids);
}
}
void DeskSyncBridge::OnStoreCreated(
const absl::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::ModelTypeStore> store) {
if (error) {
change_processor()->ReportError(*error);
return;
}
auto stored_desk_templates = std::make_unique<DeskEntries>();
DeskEntries* stored_desk_templates_copy = stored_desk_templates.get();
store_ = std::move(store);
store_->ReadAllDataAndPreprocess(
base::BindOnce(&ParseDeskTemplatesOnBackendSequence,
base::Unretained(stored_desk_templates_copy)),
base::BindOnce(&DeskSyncBridge::OnReadAllData,
weak_ptr_factory_.GetWeakPtr(),
std::move(stored_desk_templates)));
}
void DeskSyncBridge::OnReadAllData(
std::unique_ptr<DeskEntries> stored_desk_templates,
const absl::optional<syncer::ModelError>& error) {
DCHECK(stored_desk_templates);
if (error) {
change_processor()->ReportError(*error);
return;
}
entries_ = std::move(*stored_desk_templates);
store_->ReadAllMetadata(base::BindOnce(&DeskSyncBridge::OnReadAllMetadata,
weak_ptr_factory_.GetWeakPtr()));
}
void DeskSyncBridge::OnReadAllMetadata(
const absl::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
if (error) {
change_processor()->ReportError(*error);
return;
}
change_processor()->ModelReadyToSync(std::move(metadata_batch));
is_ready_ = true;
NotifyDeskModelLoaded();
}
void DeskSyncBridge::OnCommit(const absl::optional<syncer::ModelError>& error) {
if (error) {
change_processor()->ReportError(*error);
}
}
void DeskSyncBridge::Commit(std::unique_ptr<ModelTypeStore::WriteBatch> batch) {
store_->CommitWriteBatch(std::move(batch),
base::BindOnce(&DeskSyncBridge::OnCommit,
weak_ptr_factory_.GetWeakPtr()));
}
void DeskSyncBridge::UploadLocalOnlyData(
syncer::MetadataChangeList* metadata_change_list,
const syncer::EntityChangeList& entity_data) {
std::set<base::GUID> local_keys_to_upload;
for (const auto& it : entries_) {
DCHECK_EQ(DeskTemplateSource::kUser, it.second->source());
local_keys_to_upload.insert(it.first);
}
// Strip |local_keys_to_upload| of any key (UUID) that is already known to the
// server.
for (const std::unique_ptr<syncer::EntityChange>& change : entity_data) {
local_keys_to_upload.erase(
base::GUID::ParseCaseInsensitive(change->storage_key()));
}
// Upload the local-only templates.
for (const base::GUID& uuid : local_keys_to_upload) {
change_processor()->Put(uuid.AsLowercaseString(),
CopyToEntityData(ToSyncProto(entries_[uuid].get())),
metadata_change_list);
}
}
bool DeskSyncBridge::HasUserTemplateWithName(const std::u16string& name) {
return std::find_if(
entries_.begin(), entries_.end(),
[&name](std::pair<const base::GUID,
std::unique_ptr<ash::DeskTemplate>>& entry) {
return entry.second->template_name() == name;
}) != entries_.end();
}
} // namespace desks_storage