blob: d254946e1cb7b602cf03207edc56c65214e700c0 [file] [log] [blame]
// Copyright 2013 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/ui/app_list/app_list_syncable_service.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/chromeos/arc/arc_util.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/chromeos/file_manager/app_id.h"
#include "chrome/browser/chromeos/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/app_list_client_impl.h"
#include "chrome/browser/ui/app_list/app_list_model_updater.h"
#include "chrome/browser/ui/app_list/app_service_app_model_builder.h"
#include "chrome/browser/ui/app_list/arc/arc_app_item.h"
#include "chrome/browser/ui/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ui/app_list/arc/arc_app_model_builder.h"
#include "chrome/browser/ui/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ui/app_list/chrome_app_list_item.h"
#include "chrome/browser/ui/app_list/chrome_app_list_model_updater.h"
#include "chrome/browser/ui/app_list/crostini/crostini_app_model_builder.h"
#include "chrome/browser/ui/app_list/extension_app_item.h"
#include "chrome/browser/ui/app_list/extension_app_model_builder.h"
#include "chrome/browser/ui/app_list/internal_app/internal_app_model_builder.h"
#include "chrome/browser/ui/app_list/page_break_app_item.h"
#include "chrome/browser/ui/app_list/page_break_constants.h"
#include "chrome/browser/ui/app_list/plugin_vm/plugin_vm_app_model_builder.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/sync/model/sync_change_processor.h"
#include "components/sync/model/sync_data.h"
#include "components/sync/model/sync_merge_result.h"
#include "components/sync/protocol/sync.pb.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/uninstall_reason.h"
#include "extensions/common/constants.h"
#include "extensions/common/one_shot_event.h"
#include "ui/base/l10n/l10n_util.h"
using syncer::SyncChange;
namespace app_list {
namespace {
constexpr char kNameKey[] = "name";
constexpr char kParentIdKey[] = "parent_id";
constexpr char kPositionKey[] = "position";
constexpr char kPinPositionKey[] = "pin_position";
constexpr char kTypeKey[] = "type";
void GetSyncSpecificsFromSyncItem(const AppListSyncableService::SyncItem* item,
sync_pb::AppListSpecifics* specifics) {
DCHECK(specifics);
specifics->set_item_id(item->item_id);
specifics->set_item_type(item->item_type);
specifics->set_item_name(item->item_name);
specifics->set_parent_id(item->parent_id);
specifics->set_item_ordinal(item->item_ordinal.IsValid()
? item->item_ordinal.ToInternalValue()
: std::string());
specifics->set_item_pin_ordinal(item->item_pin_ordinal.IsValid()
? item->item_pin_ordinal.ToInternalValue()
: std::string());
}
syncer::SyncData GetSyncDataFromSyncItem(
const AppListSyncableService::SyncItem* item) {
sync_pb::EntitySpecifics specifics;
GetSyncSpecificsFromSyncItem(item, specifics.mutable_app_list());
return syncer::SyncData::CreateLocalData(item->item_id, item->item_id,
specifics);
}
bool AppIsDefault(extensions::ExtensionService* service,
const std::string& id) {
return service && extensions::ExtensionPrefs::Get(service->profile())
->WasInstalledByDefault(id);
}
bool IsUnRemovableDefaultApp(const std::string& id) {
return id == extension_misc::kChromeAppId ||
id == extensions::kWebStoreAppId ||
id == file_manager::kFileManagerAppId ||
id == extension_misc::kGeniusAppId;
}
void UninstallExtension(extensions::ExtensionService* service,
const std::string& id) {
if (service && service->GetInstalledExtension(id)) {
service->UninstallExtension(id, extensions::UNINSTALL_REASON_SYNC,
nullptr /* error */);
}
}
sync_pb::AppListSpecifics::AppListItemType GetAppListItemType(
const ChromeAppListItem* item) {
if (item->is_folder())
return sync_pb::AppListSpecifics::TYPE_FOLDER;
else if (item->is_page_break())
return sync_pb::AppListSpecifics::TYPE_PAGE_BREAK;
else
return sync_pb::AppListSpecifics::TYPE_APP;
}
void RemoveSyncItemFromLocalStorage(Profile* profile,
const std::string& item_id) {
DictionaryPrefUpdate(profile->GetPrefs(), prefs::kAppListLocalState)
->Remove(item_id, nullptr);
}
void UpdateSyncItemInLocalStorage(
Profile* profile,
const AppListSyncableService::SyncItem* sync_item) {
DictionaryPrefUpdate pref_update(profile->GetPrefs(),
prefs::kAppListLocalState);
base::Value* dict_item = pref_update->FindKeyOfType(
sync_item->item_id, base::Value::Type::DICTIONARY);
if (!dict_item) {
dict_item = pref_update->SetKey(sync_item->item_id,
base::Value(base::Value::Type::DICTIONARY));
}
dict_item->SetKey(kNameKey, base::Value(sync_item->item_name));
dict_item->SetKey(kParentIdKey, base::Value(sync_item->parent_id));
dict_item->SetKey(kPositionKey,
base::Value(sync_item->item_ordinal.IsValid()
? sync_item->item_ordinal.ToInternalValue()
: std::string()));
dict_item->SetKey(
kPinPositionKey,
base::Value(sync_item->item_pin_ordinal.IsValid()
? sync_item->item_pin_ordinal.ToInternalValue()
: std::string()));
dict_item->SetKey(kTypeKey,
base::Value(static_cast<int>(sync_item->item_type)));
}
AppListSyncableService::ModelUpdaterFactoryCallback*
g_model_updater_factory_callback_for_test_ = nullptr;
// Returns true if the sync item does not have parent.
bool IsTopLevelAppItem(const AppListSyncableService::SyncItem& sync_item) {
return sync_item.parent_id.empty();
}
// Returns true if the sync item is a page break item.
bool IsPageBreakItem(const AppListSyncableService::SyncItem& sync_item) {
return sync_item.item_type == sync_pb::AppListSpecifics::TYPE_PAGE_BREAK;
}
} // namespace
// AppListSyncableService::ScopedModelUpdaterFactoryForTest
AppListSyncableService::ScopedModelUpdaterFactoryForTest::
ScopedModelUpdaterFactoryForTest(
const ModelUpdaterFactoryCallback& factory) {
DCHECK(factory);
factory_ = factory;
g_model_updater_factory_callback_for_test_ = &factory_;
}
AppListSyncableService::ScopedModelUpdaterFactoryForTest::
~ScopedModelUpdaterFactoryForTest() {
DCHECK_EQ(&factory_, g_model_updater_factory_callback_for_test_);
g_model_updater_factory_callback_for_test_ = nullptr;
}
// AppListSyncableService::SyncItem
AppListSyncableService::SyncItem::SyncItem(
const std::string& id,
sync_pb::AppListSpecifics::AppListItemType type)
: item_id(id), item_type(type) {}
AppListSyncableService::SyncItem::~SyncItem() = default;
// AppListSyncableService::ModelUpdaterDelegate
class AppListSyncableService::ModelUpdaterDelegate
: public AppListModelUpdaterDelegate {
public:
explicit ModelUpdaterDelegate(AppListSyncableService* owner) : owner_(owner) {
DVLOG(2) << owner_ << ": ModelUpdaterDelegate Added";
owner_->GetModelUpdater()->SetDelegate(this);
}
~ModelUpdaterDelegate() override {
owner_->GetModelUpdater()->SetDelegate(nullptr);
DVLOG(2) << owner_ << ": ModelUpdaterDelegate Removed";
}
private:
// ChromeAppListModelUpdaterDelegate
void OnAppListItemAdded(ChromeAppListItem* item) override {
DCHECK(adding_item_id_.empty());
adding_item_id_ = item->id(); // Ignore updates while adding an item.
VLOG(2) << owner_ << " OnAppListItemAdded: " << item->ToDebugString();
owner_->AddOrUpdateFromSyncItem(item);
adding_item_id_.clear();
// Sync OEM name if it was created on demand on ash side.
if (item->id() == ash::kOemFolderId &&
item->name() != owner_->oem_folder_name_) {
item->SetName(owner_->oem_folder_name_);
}
}
void OnAppListItemWillBeDeleted(ChromeAppListItem* item) override {
DCHECK(adding_item_id_.empty());
VLOG(2) << owner_ << " OnAppListItemDeleted: " << item->ToDebugString();
// Don't sync folder removal in case the folder still exists on another
// device (e.g. with device specific items in it). Empty folders will be
// deleted when the last item is removed (in PruneEmptySyncFolders()).
if (item->is_folder())
return;
if (item->GetItemType() == ArcAppItem::kItemType) {
// Don't sync remove changes coming as result of disabling ARC.
if (!arc::IsArcPlayStoreEnabledForProfile(owner_->profile()))
return;
}
owner_->RemoveSyncItem(item->id());
}
void OnAppListItemUpdated(ChromeAppListItem* item) override {
if (!adding_item_id_.empty()) {
// Adding an item may trigger update notifications which should be
// ignored.
DCHECK_EQ(adding_item_id_, item->id());
return;
}
VLOG(2) << owner_ << " OnAppListItemUpdated: " << item->ToDebugString();
owner_->UpdateSyncItem(item);
}
AppListSyncableService* owner_;
std::string adding_item_id_;
DISALLOW_COPY_AND_ASSIGN(ModelUpdaterDelegate);
};
// AppListSyncableService
// static
void AppListSyncableService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kAppListLocalState);
}
AppListSyncableService::AppListSyncableService(
Profile* profile,
extensions::ExtensionSystem* extension_system)
: profile_(profile),
extension_system_(extension_system),
initial_sync_data_processed_(false),
first_app_list_sync_(true),
is_app_service_enabled_(
base::FeatureList::IsEnabled(features::kAppServiceAsh)),
weak_ptr_factory_(this) {
if (g_model_updater_factory_callback_for_test_)
model_updater_ = g_model_updater_factory_callback_for_test_->Run();
else
model_updater_ = std::make_unique<ChromeAppListModelUpdater>(profile);
if (!extension_system) {
LOG(ERROR) << "AppListSyncableService created with no ExtensionSystem";
return;
}
oem_folder_name_ =
l10n_util::GetStringUTF8(IDS_APP_LIST_OEM_DEFAULT_FOLDER_NAME);
if (IsExtensionServiceReady()) {
BuildModel();
} else {
extension_system_->ready().Post(
FROM_HERE, base::Bind(&AppListSyncableService::BuildModel,
weak_ptr_factory_.GetWeakPtr()));
}
}
AppListSyncableService::~AppListSyncableService() {
// Remove observers.
model_updater_delegate_.reset();
}
bool AppListSyncableService::IsExtensionServiceReady() const {
return extension_system_->extension_service() &&
extension_system_->extension_service()->is_ready();
}
void AppListSyncableService::InitFromLocalStorage() {
// This should happen before sync and model is built.
DCHECK(!sync_processor_.get());
DCHECK(!IsInitialized());
// Restore initial state from local storage.
const base::DictionaryValue* local_items =
profile_->GetPrefs()->GetDictionary(prefs::kAppListLocalState);
DCHECK(local_items);
for (base::DictionaryValue::Iterator item(*local_items); !item.IsAtEnd();
item.Advance()) {
const base::DictionaryValue* dict_item;
if (!item.value().GetAsDictionary(&dict_item)) {
LOG(ERROR) << "Dictionary not found for " << item.key() + ".";
continue;
}
int type;
if (!dict_item->GetInteger(kTypeKey, &type)) {
LOG(ERROR) << "Item type is not set in local storage for " << item.key()
<< ".";
continue;
}
SyncItem* sync_item = CreateSyncItem(
item.key(),
static_cast<sync_pb::AppListSpecifics::AppListItemType>(type));
dict_item->GetString(kNameKey, &sync_item->item_name);
dict_item->GetString(kParentIdKey, &sync_item->parent_id);
std::string position;
std::string pin_position;
dict_item->GetString(kPositionKey, &position);
dict_item->GetString(kPinPositionKey, &pin_position);
if (!position.empty())
sync_item->item_ordinal = syncer::StringOrdinal(position);
if (!pin_position.empty())
sync_item->item_pin_ordinal = syncer::StringOrdinal(pin_position);
ProcessNewSyncItem(sync_item);
}
}
bool AppListSyncableService::IsInitialized() const {
if (is_app_service_enabled_)
return app_service_apps_builder_.get();
return ext_apps_builder_.get();
}
void AppListSyncableService::BuildModel() {
InitFromLocalStorage();
DCHECK(IsExtensionServiceReady());
AppListClientImpl* client = AppListClientImpl::GetInstance();
AppListControllerDelegate* controller = client;
if (is_app_service_enabled_) {
app_service_apps_builder_ =
std::make_unique<AppServiceAppModelBuilder>(controller);
} else {
ext_apps_builder_ = std::make_unique<ExtensionAppModelBuilder>(controller);
if (arc::IsArcAllowedForProfile(profile_))
arc_apps_builder_ = std::make_unique<ArcAppModelBuilder>(controller);
if (crostini::IsCrostiniUIAllowedForProfile(profile_)) {
crostini_apps_builder_ =
std::make_unique<CrostiniAppModelBuilder>(controller);
}
if (plugin_vm::IsPluginVmAllowedForProfile(profile_)) {
plugin_vm_apps_builder_ =
std::make_unique<PluginVmAppModelBuilder>(controller);
}
internal_apps_builder_ =
std::make_unique<InternalAppModelBuilder>(controller);
}
DCHECK(profile_);
SyncStarted();
if (is_app_service_enabled_) {
app_service_apps_builder_->Initialize(this, profile_, model_updater_.get());
} else {
ext_apps_builder_->Initialize(this, profile_, model_updater_.get());
if (arc_apps_builder_.get())
arc_apps_builder_->Initialize(this, profile_, model_updater_.get());
if (crostini_apps_builder_.get())
crostini_apps_builder_->Initialize(this, profile_, model_updater_.get());
if (plugin_vm_apps_builder_.get())
plugin_vm_apps_builder_->Initialize(this, profile_, model_updater_.get());
internal_apps_builder_->Initialize(this, profile_, model_updater_.get());
}
HandleUpdateFinished();
}
void AppListSyncableService::AddObserverAndStart(Observer* observer) {
observer_list_.AddObserver(observer);
SyncStarted();
}
void AppListSyncableService::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void AppListSyncableService::NotifyObserversSyncUpdated() {
for (auto& observer : observer_list_)
observer.OnSyncModelUpdated();
}
size_t AppListSyncableService::GetNumSyncItemsForTest() {
DCHECK(IsInitialized());
return sync_items_.size();
}
const AppListSyncableService::SyncItem* AppListSyncableService::GetSyncItem(
const std::string& id) const {
auto iter = sync_items_.find(id);
if (iter != sync_items_.end())
return iter->second.get();
return NULL;
}
bool AppListSyncableService::TransferItemAttributes(
const std::string& from_app_id,
const std::string& to_app_id) {
const SyncItem* from_item = FindSyncItem(from_app_id);
if (!from_item ||
from_item->item_type != sync_pb::AppListSpecifics::TYPE_APP) {
return false;
}
auto attributes = std::make_unique<SyncItem>(
from_app_id, sync_pb::AppListSpecifics::TYPE_APP);
attributes->parent_id = from_item->parent_id;
attributes->item_ordinal = from_item->item_ordinal;
attributes->item_pin_ordinal = from_item->item_pin_ordinal;
SyncItem* to_item = FindSyncItem(to_app_id);
if (to_item) {
// |to_app_id| already exists. Can apply attributes right now.
ApplyAppAttributes(to_app_id, std::move(attributes));
} else {
// |to_app_id| does not exist at this moment. Store attributes to apply it
// later once app appears on this device.
pending_transfer_map_[to_app_id] = std::move(attributes);
}
return true;
}
void AppListSyncableService::ApplyAppAttributes(
const std::string& app_id,
std::unique_ptr<SyncItem> attributes) {
SyncItem* item = FindSyncItem(app_id);
if (!item || item->item_type != sync_pb::AppListSpecifics::TYPE_APP) {
LOG(ERROR) << "Failed to apply app attributes, app " << app_id
<< " does not exist.";
return;
}
HandleUpdateStarted();
item->parent_id = attributes->parent_id;
item->item_ordinal = attributes->item_ordinal;
item->item_pin_ordinal = attributes->item_pin_ordinal;
UpdateSyncItemInLocalStorage(profile_, item);
SendSyncChange(item, SyncChange::ACTION_UPDATE);
ProcessExistingSyncItem(item);
HandleUpdateFinished();
}
void AppListSyncableService::SetOemFolderName(const std::string& name) {
oem_folder_name_ = name;
// Update OEM folder item if it was already created. If it is not created yet
// then on creation it will take right name.
ChromeAppListItem* oem_folder_item =
model_updater_->FindItem(ash::kOemFolderId);
if (oem_folder_item)
oem_folder_item->SetName(oem_folder_name_);
}
AppListModelUpdater* AppListSyncableService::GetModelUpdater() {
return model_updater_.get();
}
void AppListSyncableService::HandleUpdateStarted() {
// Don't observe the model while processing update changes.
model_updater_delegate_.reset();
}
void AppListSyncableService::HandleUpdateFinished() {
// Processing an update may create folders without setting their positions.
// Resolve them now.
ResolveFolderPositions();
// Resume or start observing app list model changes.
model_updater_delegate_ = std::make_unique<ModelUpdaterDelegate>(this);
NotifyObserversSyncUpdated();
}
void AppListSyncableService::AddItem(
std::unique_ptr<ChromeAppListItem> app_item) {
SyncItem* sync_item = FindOrAddSyncItem(app_item.get());
if (!sync_item)
return; // Item is not valid.
if (AppIsOem(app_item->id())) {
VLOG(2) << this << ": AddItem to OEM folder: " << sync_item->ToString();
model_updater_->AddItemToOemFolder(
std::move(app_item), FindSyncItem(ash::kOemFolderId), oem_folder_name_,
GetPreferredOemFolderPos());
} else {
std::string folder_id = sync_item->parent_id;
VLOG(2) << this << ": AddItem: " << sync_item->ToString() << " Folder: '"
<< folder_id << "'";
model_updater_->AddItemToFolder(std::move(app_item), folder_id);
}
PruneRedundantPageBreakItems();
}
AppListSyncableService::SyncItem* AppListSyncableService::FindOrAddSyncItem(
const ChromeAppListItem* app_item) {
const std::string& item_id = app_item->id();
if (item_id.empty()) {
LOG(ERROR) << "ChromeAppListItem item with empty ID";
return NULL;
}
SyncItem* sync_item = FindSyncItem(item_id);
if (sync_item) {
// If there is an existing, non-REMOVE_DEFAULT entry, return it.
if (sync_item->item_type !=
sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP) {
DVLOG(2) << this << ": AddItem already exists: " << sync_item->ToString();
return sync_item;
}
if (RemoveDefaultApp(app_item, sync_item))
return NULL;
// Fall through. The REMOVE_DEFAULT_APP entry has been deleted, now a new
// App entry can be added.
}
return CreateSyncItemFromAppItem(app_item);
}
AppListSyncableService::SyncItem*
AppListSyncableService::CreateSyncItemFromAppItem(
const ChromeAppListItem* app_item) {
sync_pb::AppListSpecifics::AppListItemType type =
GetAppListItemType(app_item);
VLOG(2) << this << " CreateSyncItemFromAppItem:" << app_item->ToDebugString();
SyncItem* sync_item = CreateSyncItem(app_item->id(), type);
DCHECK(app_item->position().IsValid());
UpdateSyncItemFromAppItem(app_item, sync_item);
UpdateSyncItemInLocalStorage(profile_, sync_item);
SendSyncChange(sync_item, SyncChange::ACTION_ADD);
return sync_item;
}
syncer::StringOrdinal AppListSyncableService::GetPinPosition(
const std::string& app_id) {
SyncItem* sync_item = FindSyncItem(app_id);
if (!sync_item)
return syncer::StringOrdinal();
return sync_item->item_pin_ordinal;
}
void AppListSyncableService::SetPinPosition(
const std::string& app_id,
const syncer::StringOrdinal& item_pin_ordinal) {
// Pin position can be set only after model is built.
DCHECK(IsInitialized());
SyncItem* sync_item = FindSyncItem(app_id);
SyncChange::SyncChangeType sync_change_type;
if (sync_item) {
sync_change_type = SyncChange::ACTION_UPDATE;
} else {
sync_item = CreateSyncItem(app_id, sync_pb::AppListSpecifics::TYPE_APP);
sync_change_type = SyncChange::ACTION_ADD;
}
sync_item->item_pin_ordinal = item_pin_ordinal;
UpdateSyncItemInLocalStorage(profile_, sync_item);
SendSyncChange(sync_item, sync_change_type);
}
void AppListSyncableService::AddOrUpdateFromSyncItem(
const ChromeAppListItem* app_item) {
// Do not create a sync item for the OEM folder here, do that in
// ResolveFolderPositions once the position has been resolved.
if (app_item->id() == ash::kOemFolderId)
return;
DCHECK(app_item->position().IsValid());
SyncItem* sync_item = FindSyncItem(app_item->id());
if (sync_item) {
model_updater_->UpdateAppItemFromSyncItem(
sync_item,
sync_item->item_id !=
ash::kOemFolderId, // Don't sync oem folder's name.
false); // Don't sync its folder here.
if (!sync_item->item_ordinal.IsValid()) {
UpdateSyncItem(app_item);
VLOG(2) << "Flushing position to sync item " << sync_item;
}
return;
}
CreateSyncItemFromAppItem(app_item);
}
bool AppListSyncableService::RemoveDefaultApp(const ChromeAppListItem* item,
SyncItem* sync_item) {
CHECK_EQ(sync_item->item_type,
sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP);
// If there is an existing REMOVE_DEFAULT_APP entry, and the app is
// installed as a Default app, uninstall the app instead of adding it.
if (sync_item->item_type == sync_pb::AppListSpecifics::TYPE_APP &&
AppIsDefault(extension_system_->extension_service(), item->id())) {
VLOG(2) << this
<< ": HandleDefaultApp: Uninstall: " << sync_item->ToString();
UninstallExtension(extension_system_->extension_service(), item->id());
return true;
}
// Otherwise, we are adding the app as a non-default app (i.e. an app that
// was installed by Default and removed is getting installed explicitly by
// the user), so delete the REMOVE_DEFAULT_APP.
DeleteSyncItem(sync_item->item_id);
return false;
}
void AppListSyncableService::DeleteSyncItem(const std::string& item_id) {
SyncItem* sync_item = FindSyncItem(item_id);
if (!sync_item) {
LOG(ERROR) << "DeleteSyncItem: no sync item: " << item_id;
return;
}
if (SyncStarted()) {
VLOG(2) << this << " -> SYNC DELETE: " << sync_item->ToString();
SyncChange sync_change(FROM_HERE, SyncChange::ACTION_DELETE,
GetSyncDataFromSyncItem(sync_item));
sync_processor_->ProcessSyncChanges(FROM_HERE,
syncer::SyncChangeList(1, sync_change));
}
RemoveSyncItemFromLocalStorage(profile_, item_id);
sync_items_.erase(item_id);
}
void AppListSyncableService::UpdateSyncItem(const ChromeAppListItem* app_item) {
SyncItem* sync_item = FindSyncItem(app_item->id());
if (!sync_item) {
LOG(ERROR) << "UpdateItem: no sync item: " << app_item->id();
return;
}
bool changed = UpdateSyncItemFromAppItem(app_item, sync_item);
if (!changed) {
DVLOG(2) << this << " - Update: SYNC NO CHANGE: " << sync_item->ToString();
return;
}
UpdateSyncItemInLocalStorage(profile_, sync_item);
SendSyncChange(sync_item, SyncChange::ACTION_UPDATE);
PruneRedundantPageBreakItems();
}
void AppListSyncableService::RemoveItem(const std::string& id) {
RemoveSyncItem(id);
model_updater_->RemoveItem(id);
PruneEmptySyncFolders();
PruneRedundantPageBreakItems();
}
void AppListSyncableService::RemoveUninstalledItem(const std::string& id) {
RemoveSyncItem(id);
model_updater_->RemoveUninstalledItem(id);
PruneEmptySyncFolders();
PruneRedundantPageBreakItems();
}
void AppListSyncableService::UpdateItem(const ChromeAppListItem* app_item) {
// Check to see if the item needs to be moved to/from the OEM folder.
bool is_oem = AppIsOem(app_item->id());
if (!is_oem && app_item->folder_id() == ash::kOemFolderId)
model_updater_->MoveItemToFolder(app_item->id(), "");
else if (is_oem && app_item->folder_id() != ash::kOemFolderId)
model_updater_->MoveItemToFolder(app_item->id(), ash::kOemFolderId);
}
void AppListSyncableService::RemoveSyncItem(const std::string& id) {
VLOG(2) << this << ": RemoveSyncItem: " << id.substr(0, 8);
auto iter = sync_items_.find(id);
if (iter == sync_items_.end()) {
DVLOG(2) << this << " : RemoveSyncItem: No Item.";
return;
}
// Check for existing RemoveDefault sync item.
SyncItem* sync_item = iter->second.get();
sync_pb::AppListSpecifics::AppListItemType type = sync_item->item_type;
if (type == sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP) {
// RemoveDefault item exists, just return.
DVLOG(2) << this << " : RemoveDefault Item exists.";
return;
}
if (type == sync_pb::AppListSpecifics::TYPE_APP &&
AppIsDefault(extension_system_->extension_service(), id)) {
// This is a Default app; update the entry to a REMOVE_DEFAULT entry. This
// will overwrite any existing entry for the item.
VLOG(2) << this
<< " -> SYNC UPDATE: REMOVE_DEFAULT: " << sync_item->item_id;
sync_item->item_type = sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP;
UpdateSyncItemInLocalStorage(profile_, sync_item);
SendSyncChange(sync_item, SyncChange::ACTION_UPDATE);
return;
}
DeleteSyncItem(iter->first);
}
void AppListSyncableService::ResolveFolderPositions() {
VLOG(1) << "ResolveFolderPositions.";
for (const auto& sync_pair : sync_items_) {
SyncItem* sync_item = sync_pair.second.get();
if (sync_item->item_type != sync_pb::AppListSpecifics::TYPE_FOLDER)
continue;
model_updater_->UpdateAppItemFromSyncItem(
sync_item,
sync_item->item_id !=
ash::kOemFolderId, // Don't sync oem folder's name.
false); // Don't sync its folder here.
}
// Move the OEM folder if one exists and we have not synced its position.
if (!FindSyncItem(ash::kOemFolderId)) {
model_updater_->ResolveOemFolderPosition(
GetPreferredOemFolderPos(),
base::BindOnce(
[](base::WeakPtr<AppListSyncableService> self,
ChromeAppListItem* oem_folder) {
if (!self)
return;
if (oem_folder) {
VLOG(1) << "Creating new OEM folder sync item: "
<< oem_folder->position().ToDebugString();
self->CreateSyncItemFromAppItem(oem_folder);
}
},
weak_ptr_factory_.GetWeakPtr()));
}
}
void AppListSyncableService::PruneEmptySyncFolders() {
std::set<std::string> parent_ids;
for (const auto& sync_pair : sync_items_)
parent_ids.insert(sync_pair.second->parent_id);
for (auto iter = sync_items_.begin(); iter != sync_items_.end();) {
SyncItem* sync_item = (iter++)->second.get();
if (sync_item->item_type != sync_pb::AppListSpecifics::TYPE_FOLDER)
continue;
if (!base::ContainsKey(parent_ids, sync_item->item_id))
DeleteSyncItem(sync_item->item_id);
}
}
// AppListSyncableService syncer::SyncableService
void AppListSyncableService::InstallDefaultPageBreaksForTest() {
InstallDefaultPageBreaks();
}
syncer::SyncMergeResult AppListSyncableService::MergeDataAndStartSyncing(
syncer::ModelType type,
const syncer::SyncDataList& initial_sync_data,
std::unique_ptr<syncer::SyncChangeProcessor> sync_processor,
std::unique_ptr<syncer::SyncErrorFactory> error_handler) {
DCHECK(!sync_processor_.get());
DCHECK(sync_processor.get());
DCHECK(error_handler.get());
const bool first_time_user = initial_sync_data.empty();
if (first_time_user) {
// Post a task to avoid adding the default page break items which can cause
// sync changes during sync startup.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&AppListSyncableService::InstallDefaultPageBreaks,
weak_ptr_factory_.GetWeakPtr()));
}
HandleUpdateStarted();
// Reset local state and recreate from sync info.
DictionaryPrefUpdate pref_update(profile_->GetPrefs(),
prefs::kAppListLocalState);
pref_update->Clear();
sync_processor_ = std::move(sync_processor);
sync_error_handler_ = std::move(error_handler);
syncer::SyncMergeResult result = syncer::SyncMergeResult(type);
result.set_num_items_before_association(sync_items_.size());
VLOG(1) << this << ": MergeDataAndStartSyncing: " << initial_sync_data.size();
// Copy all sync items to |unsynced_items|.
std::set<std::string> unsynced_items;
for (const auto& sync_pair : sync_items_) {
unsynced_items.insert(sync_pair.first);
}
// Create SyncItem entries for initial_sync_data.
size_t new_items = 0, updated_items = 0;
for (syncer::SyncDataList::const_iterator iter = initial_sync_data.begin();
iter != initial_sync_data.end(); ++iter) {
const syncer::SyncData& data = *iter;
const std::string& item_id = data.GetSpecifics().app_list().item_id();
const sync_pb::AppListSpecifics& specifics = data.GetSpecifics().app_list();
DVLOG(2) << this << " Initial Sync Item: " << item_id
<< " Type: " << specifics.item_type();
DCHECK_EQ(syncer::APP_LIST, data.GetDataType());
if (ProcessSyncItemSpecifics(specifics))
++new_items;
else
++updated_items;
if (specifics.item_type() != sync_pb::AppListSpecifics::TYPE_FOLDER &&
!IsUnRemovableDefaultApp(item_id) && !AppIsOem(item_id) &&
!AppIsDefault(extension_system_->extension_service(), item_id)) {
VLOG(2) << "Syncing non-default item: " << item_id;
first_app_list_sync_ = false;
}
unsynced_items.erase(item_id);
}
result.set_num_items_after_association(sync_items_.size());
result.set_num_items_added(new_items);
result.set_num_items_deleted(0);
result.set_num_items_modified(updated_items);
// Initial sync data has been processed, it is safe now to add new sync items.
initial_sync_data_processed_ = true;
// Send unsynced items. Does not affect |result|.
syncer::SyncChangeList change_list;
for (std::set<std::string>::iterator iter = unsynced_items.begin();
iter != unsynced_items.end(); ++iter) {
SyncItem* sync_item = FindSyncItem(*iter);
// Sync can cause an item to change folders, causing an unsynced folder
// item to be removed.
if (!sync_item)
continue;
VLOG(2) << this << " -> SYNC ADD: " << sync_item->ToString();
UpdateSyncItemInLocalStorage(profile_, sync_item);
change_list.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_ADD,
GetSyncDataFromSyncItem(sync_item)));
}
// Fix items that do not contain valid app list position, required for
// builds prior to M53 (crbug.com/677647).
for (const auto& sync_pair : sync_items_) {
SyncItem* sync_item = sync_pair.second.get();
if (sync_item->item_type != sync_pb::AppListSpecifics::TYPE_APP ||
sync_item->item_ordinal.IsValid()) {
continue;
}
const ChromeAppListItem* app_item =
model_updater_->FindItem(sync_item->item_id);
if (app_item) {
if (UpdateSyncItemFromAppItem(app_item, sync_item)) {
VLOG(1) << "Fixing sync item from existing app: " << sync_item;
} else {
sync_item->item_ordinal = syncer::StringOrdinal::CreateInitialOrdinal();
VLOG(1) << "Failed to fix sync item from existing app. "
<< "Generating new position ordinal: " << sync_item;
}
} else {
sync_item->item_ordinal = syncer::StringOrdinal::CreateInitialOrdinal();
VLOG(1) << "Fixing sync item by generating new position ordinal: "
<< sync_item;
}
change_list.push_back(SyncChange(FROM_HERE, SyncChange::ACTION_UPDATE,
GetSyncDataFromSyncItem(sync_item)));
}
sync_processor_->ProcessSyncChanges(FROM_HERE, change_list);
HandleUpdateFinished();
return result;
}
void AppListSyncableService::StopSyncing(syncer::ModelType type) {
DCHECK_EQ(type, syncer::APP_LIST);
sync_processor_.reset();
sync_error_handler_.reset();
}
syncer::SyncDataList AppListSyncableService::GetAllSyncData(
syncer::ModelType type) const {
DCHECK_EQ(syncer::APP_LIST, type);
VLOG(1) << this << ": GetAllSyncData: " << sync_items_.size();
syncer::SyncDataList list;
for (auto iter = sync_items_.begin(); iter != sync_items_.end(); ++iter) {
VLOG(2) << this << " -> SYNC: " << iter->second->ToString();
list.push_back(GetSyncDataFromSyncItem(iter->second.get()));
}
return list;
}
syncer::SyncError AppListSyncableService::ProcessSyncChanges(
const base::Location& from_here,
const syncer::SyncChangeList& change_list) {
if (!sync_processor_.get()) {
return syncer::SyncError(FROM_HERE, syncer::SyncError::DATATYPE_ERROR,
"App List syncable service is not started.",
syncer::APP_LIST);
}
HandleUpdateStarted();
VLOG(1) << this << ": ProcessSyncChanges: " << change_list.size();
for (syncer::SyncChangeList::const_iterator iter = change_list.begin();
iter != change_list.end(); ++iter) {
const SyncChange& change = *iter;
VLOG(2) << this << " Change: "
<< change.sync_data().GetSpecifics().app_list().item_id() << " ("
<< change.change_type() << ")";
if (change.change_type() == SyncChange::ACTION_ADD ||
change.change_type() == SyncChange::ACTION_UPDATE) {
ProcessSyncItemSpecifics(change.sync_data().GetSpecifics().app_list());
} else if (change.change_type() == SyncChange::ACTION_DELETE) {
DeleteSyncItemSpecifics(change.sync_data().GetSpecifics().app_list());
} else {
LOG(ERROR) << "Invalid sync change";
}
}
HandleUpdateFinished();
return syncer::SyncError();
}
void AppListSyncableService::Shutdown() {
if (is_app_service_enabled_) {
app_service_apps_builder_.reset();
return;
}
internal_apps_builder_.reset();
crostini_apps_builder_.reset();
arc_apps_builder_.reset();
ext_apps_builder_.reset();
plugin_vm_apps_builder_.reset();
}
// AppListSyncableService private
bool AppListSyncableService::ProcessSyncItemSpecifics(
const sync_pb::AppListSpecifics& specifics) {
const std::string& item_id = specifics.item_id();
if (item_id.empty()) {
LOG(ERROR) << "AppList item with empty ID";
return false;
}
SyncItem* sync_item = FindSyncItem(item_id);
if (sync_item) {
// If an item of the same type exists, update it.
if (sync_item->item_type == specifics.item_type()) {
UpdateSyncItemFromSync(specifics, sync_item);
ProcessExistingSyncItem(sync_item);
UpdateSyncItemInLocalStorage(profile_, sync_item);
VLOG(2) << this << " <- SYNC UPDATE: " << sync_item->ToString();
return false;
}
// Otherwise, one of the entries should be TYPE_REMOVE_DEFAULT_APP.
if (sync_item->item_type !=
sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP &&
specifics.item_type() !=
sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP) {
LOG(ERROR) << "Synced item type: " << specifics.item_type()
<< " != existing sync item type: " << sync_item->item_type
<< " Deleting item from model!";
model_updater_->RemoveItem(item_id);
}
VLOG(2) << this << " - ProcessSyncItem: Delete existing entry: "
<< sync_item->ToString();
sync_items_.erase(item_id);
}
sync_item = CreateSyncItem(item_id, specifics.item_type());
UpdateSyncItemFromSync(specifics, sync_item);
ProcessNewSyncItem(sync_item);
UpdateSyncItemInLocalStorage(profile_, sync_item);
VLOG(2) << this << " <- SYNC ADD: " << sync_item->ToString();
return true;
}
void AppListSyncableService::ProcessNewSyncItem(SyncItem* sync_item) {
VLOG(2) << "ProcessNewSyncItem: " << sync_item->ToString();
switch (sync_item->item_type) {
case sync_pb::AppListSpecifics::TYPE_APP: {
// New apps are added through ExtensionAppModelBuilder.
// TODO(stevenjb): Determine how to handle app items in sync that
// are not installed (e.g. default / OEM apps).
return;
}
case sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP: {
VLOG(1) << this << ": Uninstall: " << sync_item->ToString();
UninstallExtension(extension_system_->extension_service(),
sync_item->item_id);
return;
}
case sync_pb::AppListSpecifics::TYPE_FOLDER: {
// We don't create new folders here, the model will do that.
model_updater_->UpdateAppItemFromSyncItem(
sync_item,
sync_item->item_id !=
ash::kOemFolderId, // Don't sync oem folder's name.
false); // It's a folder itself.
return;
}
case sync_pb::AppListSpecifics::TYPE_URL: {
// TODO(stevenjb): Implement
LOG(WARNING) << "TYPE_URL not supported";
return;
}
case sync_pb::AppListSpecifics::TYPE_PAGE_BREAK: {
// This is can be either a default page break item that was installed by
// default for new users, or a non-default page-break item that was added
// by the user. the ctor of PageBreakAppItem will update the newly-created
// item from its |sync_item|.
model_updater_->AddItem(std::make_unique<PageBreakAppItem>(
profile_, model_updater_.get(), sync_item, sync_item->item_id));
return;
}
}
NOTREACHED() << "Unrecognized sync item type: " << sync_item->ToString();
}
void AppListSyncableService::ProcessExistingSyncItem(SyncItem* sync_item) {
if (sync_item->item_type ==
sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP) {
return;
}
VLOG(2) << "ProcessExistingSyncItem: " << sync_item->ToString();
// OEM app can be moved outside folder or into non-OEM folder, so always sync
// the position.
model_updater_->UpdateAppItemFromSyncItem(
sync_item,
sync_item->item_id != ash::kOemFolderId, // Don't sync oem folder's name.
true /* update_folder */);
}
bool AppListSyncableService::SyncStarted() {
if (sync_processor_.get())
return true;
if (flare_.is_null()) {
VLOG(1) << this << ": SyncStarted: Flare.";
flare_ = sync_start_util::GetFlareForSyncableService(profile_->GetPath());
flare_.Run(syncer::APP_LIST);
}
return false;
}
void AppListSyncableService::SendSyncChange(
SyncItem* sync_item,
SyncChange::SyncChangeType sync_change_type) {
if (!SyncStarted()) {
DVLOG(2) << this << " - SendSyncChange: SYNC NOT STARTED: "
<< sync_item->ToString();
return;
}
if (!initial_sync_data_processed_ &&
sync_change_type == SyncChange::ACTION_ADD) {
// This can occur if an initial item is created before its folder item.
// A sync item should already exist for the folder, so we do not want to
// send an ADD event, since that would trigger a CHECK in the sync code.
DCHECK(sync_item->item_type == sync_pb::AppListSpecifics::TYPE_FOLDER);
DVLOG(2) << this << " - SendSyncChange: ADD before initial data processed: "
<< sync_item->ToString();
return;
}
if (sync_change_type == SyncChange::ACTION_ADD)
VLOG(2) << this << " -> SYNC ADD: " << sync_item->ToString();
else
VLOG(2) << this << " -> SYNC UPDATE: " << sync_item->ToString();
SyncChange sync_change(FROM_HERE, sync_change_type,
GetSyncDataFromSyncItem(sync_item));
sync_processor_->ProcessSyncChanges(FROM_HERE,
syncer::SyncChangeList(1, sync_change));
}
AppListSyncableService::SyncItem* AppListSyncableService::FindSyncItem(
const std::string& item_id) {
auto iter = sync_items_.find(item_id);
if (iter == sync_items_.end())
return NULL;
return iter->second.get();
}
AppListSyncableService::SyncItem* AppListSyncableService::CreateSyncItem(
const std::string& item_id,
sync_pb::AppListSpecifics::AppListItemType item_type) {
DCHECK(!base::ContainsKey(sync_items_, item_id));
sync_items_[item_id] = std::make_unique<SyncItem>(item_id, item_type);
// In case we have pending attributes to apply, process it asynchronously.
if (base::ContainsKey(pending_transfer_map_, item_id)) {
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&AppListSyncableService::ApplyAppAttributes,
weak_ptr_factory_.GetWeakPtr(), item_id,
std::move(pending_transfer_map_[item_id])));
pending_transfer_map_.erase(item_id);
}
return sync_items_[item_id].get();
}
void AppListSyncableService::DeleteSyncItemSpecifics(
const sync_pb::AppListSpecifics& specifics) {
const std::string& item_id = specifics.item_id();
if (item_id.empty()) {
LOG(ERROR) << "Delete AppList item with empty ID";
return;
}
VLOG(2) << this << ": DeleteSyncItemSpecifics: " << item_id.substr(0, 8);
auto iter = sync_items_.find(item_id);
if (iter == sync_items_.end())
return;
sync_pb::AppListSpecifics::AppListItemType item_type =
iter->second->item_type;
VLOG(2) << this << " <- SYNC DELETE: " << iter->second->ToString();
RemoveSyncItemFromLocalStorage(profile_, item_id);
sync_items_.erase(iter);
// Only delete apps from the model. Folders will be deleted when all
// children have been deleted.
if (item_type == sync_pb::AppListSpecifics::TYPE_APP) {
model_updater_->RemoveItem(item_id);
}
}
syncer::StringOrdinal AppListSyncableService::GetPreferredOemFolderPos() {
VLOG(1) << "GetPreferredOemFolderPos: " << first_app_list_sync_;
if (!first_app_list_sync_) {
VLOG(1) << "Sync items exist, placing OEM folder at end.";
syncer::StringOrdinal last;
for (const auto& sync_pair : sync_items_) {
SyncItem* sync_item = sync_pair.second.get();
if (sync_item->item_ordinal.IsValid() &&
(!last.IsValid() || sync_item->item_ordinal.GreaterThan(last))) {
last = sync_item->item_ordinal;
}
}
if (last.IsValid())
return last.CreateAfter();
}
return syncer::StringOrdinal();
}
bool AppListSyncableService::AppIsOem(const std::string& id) {
const ArcAppListPrefs* arc_prefs = ArcAppListPrefs::Get(profile_);
if (arc_prefs && arc_prefs->IsOem(id))
return true;
if (!extension_system_->extension_service())
return false;
const extensions::Extension* extension =
extension_system_->extension_service()->GetExtensionById(id, true);
return extension && extension->was_installed_by_oem();
}
std::string AppListSyncableService::SyncItem::ToString() const {
std::string res = item_id.substr(0, 8);
if (item_type == sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP) {
res += " { RemoveDefault }";
} else if (item_type == sync_pb::AppListSpecifics::TYPE_PAGE_BREAK) {
res += " { PageBreakItem }";
res += " [" + item_ordinal.ToDebugString() + "]";
} else {
res += " { " + item_name + " }";
res += " [" + item_ordinal.ToDebugString() + "]";
if (!parent_id.empty())
res += " <" + parent_id.substr(0, 8) + ">";
res += " [" + item_pin_ordinal.ToDebugString() + "]";
}
return res;
}
std::vector<AppListSyncableService::SyncItem*>
AppListSyncableService::GetSortedTopLevelSyncItems() const {
// Filter out items in folder.
std::vector<SyncItem*> sync_items;
for (const auto& sync_pair : sync_items_) {
const auto* sync_item = sync_pair.second.get();
if (IsTopLevelAppItem(*sync_item) && sync_item->item_ordinal.IsValid())
sync_items.emplace_back(sync_pair.second.get());
}
// Sort remaining items based on their positions.
std::sort(sync_items.begin(), sync_items.end(),
[](SyncItem* const& item1, SyncItem* const& item2) -> bool {
return item1->item_ordinal.LessThan(item2->item_ordinal);
});
return sync_items;
}
void AppListSyncableService::PruneRedundantPageBreakItems() {
auto top_level_sync_items = GetSortedTopLevelSyncItems();
// If the first item is a "page break" item, delete it. If there are
// contiguous "page break" items, delete duplicate.
bool was_page_break = true;
for (auto iter = top_level_sync_items.begin();
iter != top_level_sync_items.end();) {
if (!IsPageBreakItem(**iter)) {
was_page_break = false;
++iter;
continue;
}
auto current_iter = iter++;
if (was_page_break) {
DeleteSyncItem((*current_iter)->item_id);
iter = top_level_sync_items.erase(current_iter);
} else {
was_page_break = true;
}
}
// Remove the trailing "page break" item if it exists.
if (!top_level_sync_items.empty() &&
IsPageBreakItem(*top_level_sync_items.back())) {
DeleteSyncItem(top_level_sync_items.back()->item_id);
}
// Remove all the "page break" items that are in folder. No such item should
// exist in folder. It should be safe to remove them if it do occur.
for (auto iter = sync_items_.begin(); iter != sync_items_.end();) {
const auto* sync_item = (iter++)->second.get();
if (IsTopLevelAppItem(*sync_item) || !IsPageBreakItem(*sync_item))
continue;
LOG(ERROR) << "Delete a page break item in folder: " << sync_item->item_id;
DeleteSyncItem(sync_item->item_id);
}
}
void AppListSyncableService::InstallDefaultPageBreaks() {
for (size_t i = 0; i < app_list::kDefaultPageBreakAppIdsLength; ++i) {
auto* const id = app_list::kDefaultPageBreakAppIds[i];
auto* sync_item = GetSyncItem(id);
if (sync_item) {
// The user may have cleared their sync from
// https://chrome.google.com/sync, so it may appear here that it's a new
// user, while in fact on this device, it's not. We don't want to recreate
// and re-add an already existing default page break item.
continue;
}
auto page_break_item = std::make_unique<PageBreakAppItem>(
profile(), model_updater_.get(), nullptr /* sync_item */, id);
page_break_item->SetName("__default_page_break__");
AddItem(std::move(page_break_item));
}
}
void AppListSyncableService::UpdateSyncItemFromSync(
const sync_pb::AppListSpecifics& specifics,
AppListSyncableService::SyncItem* item) {
DCHECK_EQ(item->item_id, specifics.item_id());
item->item_type = specifics.item_type();
item->item_name = specifics.item_name();
// Ignore update to put item into the OEM folder in case app is not OEM. This
// can happen when app is installed on several devices where app is OEM on one
// device and not on another devices.
if (specifics.parent_id() != ash::kOemFolderId || AppIsOem(item->item_id))
item->parent_id = specifics.parent_id();
if (specifics.has_item_ordinal())
item->item_ordinal = syncer::StringOrdinal(specifics.item_ordinal());
if (specifics.has_item_pin_ordinal()) {
item->item_pin_ordinal =
syncer::StringOrdinal(specifics.item_pin_ordinal());
}
}
bool AppListSyncableService::UpdateSyncItemFromAppItem(
const ChromeAppListItem* app_item,
AppListSyncableService::SyncItem* sync_item) {
DCHECK_EQ(sync_item->item_id, app_item->id());
// Page breaker should not be added in a folder.
DCHECK(!app_item->is_page_break() || app_item->folder_id().empty());
bool changed = false;
// Allow sync changes for parent only for non OEM app.
if (sync_item->parent_id != app_item->folder_id() &&
!AppIsOem(app_item->id())) {
sync_item->parent_id = app_item->folder_id();
changed = true;
}
if (sync_item->item_name != app_item->name()) {
sync_item->item_name = app_item->name();
changed = true;
}
if (!sync_item->item_ordinal.IsValid() ||
!app_item->position().Equals(sync_item->item_ordinal)) {
sync_item->item_ordinal = app_item->position();
changed = true;
}
return changed;
}
} // namespace app_list