// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/app_list/app_list_sync_model_sanitizer.h"
#include <vector>
#include "ash/public/cpp/app_list/app_list_config.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/strings/string_util.h"
#include "base/uuid.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
#include "components/sync/model/string_ordinal.h"
namespace app_list {
namespace {
// Prefix added to IDs of page breaks created by `AppListSyncModelSanitizer`.
// The prexis ensures that sanitizer can easily identify page breaks created
// during previous sanitization passes (and which can freely be removed during
// sanitization later on), without substantially changing the page breaks app
// list item sync data so page breaks can be recognized on older chrome versions
// that do not have "implicit" page breaks.
constexpr char kImplicitPageBreakIdPrefix[] = "Implicit###";
} // namespace
AppListSyncModelSanitizer::AppListSyncModelSanitizer(
AppListSyncableService* syncable_service)
: syncable_service_(syncable_service) {}
AppListSyncModelSanitizer::~AppListSyncModelSanitizer() = default;
void AppListSyncModelSanitizer::SanitizePageBreaks(
const std::set<std::string>& top_level_items,
bool reset_page_breaks) {
const std::vector<AppListSyncableService::SyncItem*> sync_items =
syncable_service_->GetSortedTopLevelSyncItems();
std::vector<std::string> page_breaks_to_remove;
std::vector<syncer::StringOrdinal> page_breaks_to_add;
std::map<std::string, syncer::StringOrdinal> resolved_duplicate_positions;
int current_page_size = 0;
syncer::StringOrdinal last_valid_position;
for (size_t i = 0; i < sync_items.size(); ++i) {
const AppListSyncableService::SyncItem* item = sync_items[i];
const std::string item_id = item->item_id;
syncer::StringOrdinal item_ordinal =
resolved_duplicate_positions.count(item_id)
? resolved_duplicate_positions[item_id]
: item->item_ordinal;
// `AppListSyncableService::GetSortedTopLevelSyncItems()` filters out items
// with an invalid position.
DCHECK(item_ordinal.IsValid());
if (item->item_type == sync_pb::AppListSpecifics::TYPE_PAGE_BREAK) {
const bool implicit_page_break =
base::StartsWith(item_id, kImplicitPageBreakIdPrefix);
if (reset_page_breaks && !implicit_page_break) {
// When resetting page breaks, remove non-implicit page breaks.
// If the page break is required at this position, a new implicit page
// break will be created later on, with the difference that the new page
// break will be removable by he sanitizer.
page_breaks_to_remove.push_back(item_id);
continue;
}
// If the page break is after a full page, leave it in place.
if (!implicit_page_break ||
current_page_size ==
ash::SharedAppListConfig::instance().GetMaxNumOfItemsPerPage()) {
current_page_size = 0;
last_valid_position = item_ordinal;
continue;
}
// If page is not yet full, and the page break was added to sanitize page
// size (i.e. were not a result of an explicit user action in pre
// productivity launcher), remove the page break.
page_breaks_to_remove.push_back(item_id);
continue;
}
if (item->item_type != sync_pb::AppListSpecifics::TYPE_FOLDER &&
item->item_type != sync_pb::AppListSpecifics::TYPE_APP) {
last_valid_position = item_ordinal;
continue;
}
// Ignore app list items that are not installed locally - this has potential
// to cause page overflow within sync data on certain devices,but it better
// matches legacy behavior where page structure was primarily based on local
// model, and results in better page structure on the devices with similar
// set of installed apps and the productivity launcher feature disabled
// (including the case where productivity launcher feature is toggled on the
// same device).
// Alternatively, this method could calculate page size purely from sync
// data - this would ensure consistent mapping from an app to a page, but
// could also unexpectedly create partially filled pages where they did not
// previously exist (for example, if sync contains items that are not
// installed on a portion of the user's devices).
if (!base::Contains(top_level_items, item_id)) {
last_valid_position = item_ordinal;
continue;
}
// The item overflows the current page - add a page break just before it.
if (current_page_size ==
ash::SharedAppListConfig::instance().GetMaxNumOfItemsPerPage()) {
// The page break will be inserted before the current item, so the current
// page contains the current item.
current_page_size = 1;
DCHECK(last_valid_position.IsValid());
// If page break should be added between items with the same ordinal,
// deduplicate the item ordinals before calculating the new page break
// position.
if (last_valid_position.Equals(item_ordinal)) {
ResolveDuplicatePositionsStartingAtIndex(
sync_items, i, last_valid_position, &resolved_duplicate_positions);
DCHECK(resolved_duplicate_positions.count(item_id));
item_ordinal = resolved_duplicate_positions[item_id];
page_breaks_to_add.push_back(
last_valid_position.CreateBetween(item_ordinal));
} else {
page_breaks_to_add.push_back(
last_valid_position.CreateBetween(item_ordinal));
}
} else {
++current_page_size;
}
last_valid_position = item_ordinal;
}
for (const auto& resolved_position : resolved_duplicate_positions) {
syncable_service_->GetModelUpdater()->SetItemPosition(
resolved_position.first, resolved_position.second);
}
for (const auto& position : page_breaks_to_add) {
syncable_service_->AddPageBreakItem(
std::string(kImplicitPageBreakIdPrefix) +
base::Uuid::GenerateRandomV4().AsLowercaseString(),
position);
}
for (const auto& id : page_breaks_to_remove)
syncable_service_->DeleteSyncItem(id);
}
void AppListSyncModelSanitizer::ResolveDuplicatePositionsStartingAtIndex(
const std::vector<AppListSyncableService::SyncItem*>& sync_items,
size_t starting_index,
const syncer::StringOrdinal& starting_ordinal,
std::map<std::string, syncer::StringOrdinal>* resolved_positions) {
DCHECK_LT(starting_index, sync_items.size());
// Find the next position distinct from `starting_ordinal`, starting at
// `starting_index`.
syncer::StringOrdinal next_valid_position;
// Default to the case a distinct position is not found.
size_t first_distinct_index = sync_items.size();
for (size_t i = starting_index; i < sync_items.size(); ++i) {
AppListSyncableService::SyncItem* item = sync_items[i];
if (item->item_ordinal.IsValid() &&
!item->item_ordinal.Equals(starting_ordinal)) {
next_valid_position = item->item_ordinal;
first_distinct_index = i;
break;
}
}
// If all items from starting position to the end of sync items list have the
// same ordinal, next valid position may not have been set.
if (!next_valid_position.IsValid())
next_valid_position = starting_ordinal.CreateAfter();
// Generate new ordinals for all detected duplicate items.
for (size_t current_index = first_distinct_index - 1;
current_index >= starting_index; --current_index) {
next_valid_position = starting_ordinal.CreateBetween(next_valid_position);
AppListSyncableService::SyncItem* item = sync_items[current_index];
resolved_positions->emplace(item->item_id, next_valid_position);
}
}
} // namespace app_list