// 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/reorder/app_list_reorder_core.h"
#include <stack>
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/numerics/safe_conversions.h"
#include "chrome/browser/ash/app_list/app_list_model_updater.h"
#include "chrome/browser/ash/app_list/chrome_app_list_item.h"
namespace app_list {
namespace reorder {
namespace {
using SyncItem = AppListSyncableService::SyncItem;
// Indicate the status of a subsequence.
struct SubsequenceStatus {
// The subsequence length.
int length = 0;
// Used to reconstruct the subsequence.
std::optional<size_t> prev_item;
};
// Returns true if `order` is increasing.
bool IsIncreasingOrder(ash::AppListSortOrder order) {
switch (order) {
case ash::AppListSortOrder::kCustom:
NOTREACHED_IN_MIGRATION();
return false;
case ash::AppListSortOrder::kNameAlphabetical:
return true;
case ash::AppListSortOrder::kNameReverseAlphabetical:
return false;
case ash::AppListSortOrder::kColor:
return true;
case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst:
return true;
}
}
template <typename T>
void SortItems(std::vector<T>* items, ash::AppListSortOrder order);
template <>
void SortItems(std::vector<SyncItemWrapper<std::u16string>>* items,
ash::AppListSortOrder order) {
UErrorCode error = U_ZERO_ERROR;
std::unique_ptr<icu::Collator> collator(icu::Collator::createInstance(error));
if (U_FAILURE(error))
collator.reset();
StringWrapperComparator comparator(IsIncreasingOrder(order), collator.get());
std::sort(items->begin(), items->end(), comparator);
}
template <>
void SortItems(std::vector<SyncItemWrapper<ash::IconColor>>* items,
ash::AppListSortOrder order) {
DCHECK(IsIncreasingOrder(order));
IconColorWrapperComparator comparator;
std::sort(items->begin(), items->end(), comparator);
}
template <>
void SortItems(std::vector<SyncItemWrapper<EphemeralAwareName>>* items,
ash::AppListSortOrder order) {
DCHECK(IsIncreasingOrder(order));
UErrorCode error = U_ZERO_ERROR;
std::unique_ptr<icu::Collator> collator(icu::Collator::createInstance(error));
if (U_FAILURE(error))
collator.reset();
EphemeralStateAndNameComparator comparator(collator.get());
std::sort(items->begin(), items->end(), comparator);
}
// Sorts `wrappers` based on `order` and returns the longest subsequence in
// increasing ordinal order (a.k.a the longest increasing subsequence or
// "LIS"). Each element in LIS is an index of an element in `wrappers`.
template <typename T>
std::vector<int> SortAndGetLis(
ash::AppListSortOrder order,
std::vector<reorder::SyncItemWrapper<T>>* wrappers) {
DCHECK(!wrappers->empty());
SortItems(wrappers, order);
// The remaining code is needed to find the longest increasing subsequence
// (LIS) through dynamic programming. Denote the LIS ending with the j-th
// element by s(j); denote the j-th element by elements(j). Then s(j) must
// be the combination of s(i) and elements(j), where i < j and
// elements(i).ordinal < elements(j).ordinal.
// The maximum length of LIS found so far. Note that length of LIS is zero
// if all of current ordinals are invalid.
int maximum_length = 0;
// Index of LIS's ending element.
std::optional<int> lis_end_index;
// `status_array[i]` stores the status of the LIS which ends with the i-th
// element of `wrappers`.
std::vector<SubsequenceStatus> status_array(wrappers->size());
for (size_t i = 0; i < status_array.size(); ++i) {
const syncer::StringOrdinal& item_ordinal = (*wrappers)[i].item_ordinal;
// The element with the invalid ordinal should not be included in the LIS.
// Because the invalid ordinal has to be updated.
if (!item_ordinal.IsValid())
continue;
// When an increasing subsequence can be combined with i-th wrapper to
// form a new increasing subsequence, it is called "appendable".
// `optimal_prev_index` is the index to the last element of the longest
// appendable subsequence.
std::optional<size_t> optimal_prev_index;
for (size_t prev_id_index = 0; prev_id_index < i; ++prev_id_index) {
const syncer::StringOrdinal& prev_item_ordinal =
(*wrappers)[prev_id_index].item_ordinal;
// Nothing to do if LIS ending at the element corresponding to
// `prev_id_index` is not appendable.
if (!prev_item_ordinal.IsValid() ||
!item_ordinal.GreaterThan(prev_item_ordinal)) {
continue;
}
// Start a new appendable subsequence if none have yet been found.
if (!optimal_prev_index) {
optimal_prev_index = prev_id_index;
continue;
}
// Update `optimal_prev_index` only when a longer appendable subsequence
// is found.
const int prev_subsequence_length = status_array[prev_id_index].length;
const SubsequenceStatus& optimal_prev_subsequence_status =
status_array[*optimal_prev_index];
if (prev_subsequence_length > optimal_prev_subsequence_status.length)
optimal_prev_index = prev_id_index;
}
// Update `status_array`.
SubsequenceStatus& subsequence_status = status_array[i];
if (optimal_prev_index) {
subsequence_status.length = status_array[*optimal_prev_index].length + 1;
subsequence_status.prev_item = optimal_prev_index;
} else {
subsequence_status.length = 1;
}
// If a longer increasing subsequence is found, record its length and its
// last element.
if (subsequence_status.length > maximum_length) {
maximum_length = subsequence_status.length;
lis_end_index = i;
}
}
std::vector<int> lis;
std::optional<int> element_in_lis = lis_end_index;
while (element_in_lis) {
lis.push_back(*element_in_lis);
element_in_lis = status_array[*element_in_lis].prev_item;
}
// Note that before reversal the elements in `lis` are in reverse order.
// Although reversal isn't necessary, `lis` is reversed to make coding
// easier.
std::reverse(lis.begin(), lis.end());
return lis;
}
template <typename T>
void GenerateReorderParamsWithLis(
const std::vector<reorder::SyncItemWrapper<T>>& wrappers,
const std::vector<int>& lis,
std::vector<reorder::ReorderParam>* reorder_params) {
DCHECK(!wrappers.empty());
// Handle the edge case that `lis` is empty, which means that all existing
// ordinals are invalid and should be updated.
if (lis.empty()) {
for (size_t index = 0; index < wrappers.size(); ++index) {
const syncer::StringOrdinal updated_ordinal =
(index == 0 ? syncer::StringOrdinal::CreateInitialOrdinal()
: reorder_params->back().ordinal.CreateAfter());
reorder_params->emplace_back(wrappers[index].id, updated_ordinal);
}
return;
}
// Indicate the ordinal of the previous item in the sorted list.
std::optional<syncer::StringOrdinal> prev_ordinal;
// The index of the next item whose ordinal waits for update.
int index_of_item_to_update = 0;
// The index of the next element waiting for handling in `lis`.
int index_of_lis_front_element = 0;
const int wrappers_size = base::checked_cast<int>(wrappers.size());
const int lis_size = base::checked_cast<int>(lis.size());
while (index_of_item_to_update < wrappers_size) {
if (index_of_lis_front_element >= lis_size) {
// All elements in `lis` have been visited.
// The case that `lis` is empty has been handled before the loop.
// Therefore if `index_of_lis_front_element` has reached the end, the
// loop iterates at least once.
DCHECK_GT(index_of_item_to_update, 0);
// Use a bigger ordinal to ensure the increasing order.
syncer::StringOrdinal new_ordinal = prev_ordinal->CreateAfter();
reorder_params->emplace_back(wrappers[index_of_item_to_update].id,
new_ordinal);
++index_of_item_to_update;
prev_ordinal = new_ordinal;
continue;
}
const int lis_front = lis[index_of_lis_front_element];
if (index_of_item_to_update < lis_front) {
// The code below is for generating ordinals for the items mapped by the
// indices among the range of [index_of_item_to_update, lis_front).
// Use a stack to temporarily store the newly generated ordinals. It
// helps to insert elements into `reorder_params` following the
// increasing ordinal order, which makes debugging easier.
std::stack<syncer::StringOrdinal> reversely_generated_ordinals;
syncer::StringOrdinal upper_bound = wrappers[lis_front].item_ordinal;
for (int i = lis_front - 1; i >= index_of_item_to_update; --i) {
syncer::StringOrdinal new_ordinal =
prev_ordinal ? prev_ordinal->CreateBetween(upper_bound)
: upper_bound.CreateBefore();
reversely_generated_ordinals.push(new_ordinal);
upper_bound = new_ordinal;
}
for (int i = index_of_item_to_update; i < lis_front; ++i) {
reorder_params->emplace_back(wrappers[i].id,
reversely_generated_ordinals.top());
prev_ordinal = reversely_generated_ordinals.top();
reversely_generated_ordinals.pop();
}
DCHECK(reversely_generated_ordinals.empty());
index_of_item_to_update = lis_front;
continue;
}
// Note that `index_of_item_to_update` cannot be greater than `lis_front`.
// In addition, the case that `index_to_update` is smaller has been
// handled. Therefore, `index_of_item_to_update` must be equal to
// `lis_front` here.
CHECK_EQ(index_of_item_to_update, lis_front);
// No need to update the items included in `lis`.
prev_ordinal = wrappers[index_of_item_to_update].item_ordinal;
++index_of_lis_front_element;
++index_of_item_to_update;
}
}
template <typename T>
std::vector<reorder::ReorderParam> GenerateReorderParamsImpl(
ash::AppListSortOrder order,
std::vector<reorder::SyncItemWrapper<T>>* wrappers) {
std::vector<reorder::ReorderParam> reorder_params;
GenerateReorderParamsWithLis(*wrappers, SortAndGetLis(order, wrappers),
&reorder_params);
return reorder_params;
}
// Calculates the entropy (i.e. the ratio of the out-of-order item number to
// the total number) of `items` based on the specified order. Fill
// `sorted_subsequence` if `sorted_subsequence` is not null.
// Note that `items` should not be empty.
template <typename T>
void CalculateEntropyAndGetSortedSubsequence(
ash::AppListSortOrder order,
std::vector<reorder::SyncItemWrapper<T>>* items,
float* entropy,
std::vector<reorder::SyncItemWrapper<T>>* sorted_subsequence) {
DCHECK(!items->empty());
std::vector<int> lis = SortAndGetLis(order, items);
const int total_item_count = items->size();
*entropy = (total_item_count - lis.size()) / float(total_item_count);
if (!sorted_subsequence)
return;
DCHECK(sorted_subsequence->empty());
for (const int& index : lis)
sorted_subsequence->push_back(items->at(index));
}
// Calculate neighbors' locations so that if the new item is inserted between
// neighbors then `sorted_subsequence` keeps the order defined by `compare`.
// Passes the results through parameters. If the new item should be placed at
// the start or end either `prev` or `next` is empty. Note that `prev` and
// `next` are calculated based on the local items (i.e. the app list items of
// the device where a new item is added). Local items are contrast to global
// items which include all items from all sync devices.
// `compare` is comparison function object which returns true if the first
// argument is ordered before) the second.
template <typename T, class Compare>
void CalculateNeighbors(const T& item_wrapper,
const std::vector<T>& sorted_subsequence,
Compare compare,
syncer::StringOrdinal* prev,
syncer::StringOrdinal* next) {
DCHECK(prev && !prev->IsValid());
DCHECK(next && !next->IsValid());
DCHECK(!sorted_subsequence.empty());
// Find the item that should be placed right after the new item when the new
// item is added.
auto lower_bound =
std::lower_bound(sorted_subsequence.cbegin(), sorted_subsequence.cend(),
item_wrapper, compare);
// Handle the case the `item` should be placed before all other items.
if (lower_bound == sorted_subsequence.cbegin()) {
*next = lower_bound->item_ordinal;
return;
}
// Handle the case that `item` should be placed after all other items..
if (lower_bound == sorted_subsequence.cend()) {
*prev = sorted_subsequence.back().item_ordinal;
return;
}
// The `item` is placed between two other items.
*prev = std::prev(lower_bound)->item_ordinal;
*next = lower_bound->item_ordinal;
}
// Adjusts `prev` and `prev` in global scope so that the sorting order is kept
// on all sync devices after placing `new_item` between adjusted neighbors.
// `compare` is comparison function object which returns true if the first
// argument is ordered before) the second.
template <typename T, class Compare>
void AdjustNeighborsInGlobalScope(const T& new_item,
const std::vector<T>& global_items,
Compare compare,
syncer::StringOrdinal* prev,
syncer::StringOrdinal* next) {
// Before adjustment, `prev` and `next` are the new item's local neighbor
// positions (see CalculateNeighbors() for more details). Recall that
// different sync devices may have different sets of apps. This method checks
// the existing sync items whose positions are between `prev` and `next` so as
// to get the correct position in global scope.
DCHECK(prev);
DCHECK(next);
for (const auto& item : global_items) {
const syncer::StringOrdinal& position = item.item_ordinal;
if (!position.IsValid())
continue;
// Skip the loop iteration if `position` is not in the range of
// (global_prev, global_next) because it cannot shrink the range.
if ((prev->IsValid() && !position.GreaterThan(*prev)) ||
(next->IsValid() && !position.LessThan(*next))) {
continue;
}
if (compare(new_item, item)) {
// Handle the case that the new item should be placed in front of `item`.
// Note that if `item` is equal to `new_item`, `item` is always placed
// after `new_item` to keep the consistency with `CalculateNeighbors()`.
*next = position;
} else {
// Handle the case that the new item should be placed after `item`.
*prev = position;
}
}
}
syncer::StringOrdinal CalculatePositionBetweenNeighbors(
const syncer::StringOrdinal& prev,
const syncer::StringOrdinal& next) {
if (!prev.IsValid() && !next.IsValid()) {
// Not sure whether this case really exists. Handle it for satefy.
return syncer::StringOrdinal().CreateInitialOrdinal();
}
if (prev.IsValid() && next.IsValid()) {
// Both left neighbor and right neighbor are valid. Return a position that
// is between `prev` and `next`.
return prev.CreateBetween(next);
}
if (prev.IsValid()) {
// Only `prev` is valid. Return a position that is after `prev`.
return prev.CreateAfter();
}
// Only `next` is valid. Return a position that is before `next`.
return next.CreateBefore();
}
// Implementation for `CalculateItemPositionInOrder()` parameterized by type
// used to compare items. `compare` is comparison function object which returns
// true if the first argument is ordered before) the second.
template <typename T, class Compare>
bool CalculatePositionForSyncItemWrapper(
ash::AppListSortOrder order,
const reorder::SyncItemWrapper<T>& item_wrapper,
const std::vector<const ChromeAppListItem*>& local_items,
const AppListSyncableService::SyncItemMap* global_items,
Compare compare,
syncer::StringOrdinal* target_position) {
std::vector<reorder::SyncItemWrapper<T>> local_item_wrappers =
reorder::GenerateWrappersFromAppListItems<T>(local_items,
item_wrapper.id);
if (local_item_wrappers.empty()) {
*target_position = syncer::StringOrdinal::CreateInitialOrdinal();
return true;
}
float entropy;
std::vector<reorder::SyncItemWrapper<T>> sorted_subsequence;
CalculateEntropyAndGetSortedSubsequence(order, &local_item_wrappers, &entropy,
&sorted_subsequence);
if (entropy > reorder::kOrderResetThreshold) {
// Do not set `target_position` if entropy is too high.
return false;
}
syncer::StringOrdinal prev_neighbor;
syncer::StringOrdinal next_neighbor;
CalculateNeighbors(item_wrapper, sorted_subsequence, compare, &prev_neighbor,
&next_neighbor);
if (global_items) {
AdjustNeighborsInGlobalScope(
item_wrapper, reorder::GenerateWrappersFromSyncItems<T>(*global_items),
compare, &prev_neighbor, &next_neighbor);
}
// Use the item's old position if the old value does not break the item order.
if (item_wrapper.item_ordinal.IsValid()) {
const syncer::StringOrdinal& old_ordinal = item_wrapper.item_ordinal;
// `old_ordinal` maintains the order with `prev_neighbor` if:
// (1) `prev_neighbor` is empty, or
// (2) `prev_neighbor` is not greater than `old_ordinal`.
const bool is_prev_neighbor_in_order =
(!prev_neighbor.IsValid() || !prev_neighbor.GreaterThan(old_ordinal));
// `old_ordinal` maintains the order with `next_neighbor` if:
// (1) `next_neighbor` is empty, or
// (2) `next_neighbor` is not less than `old_ordinal`.
const bool is_next_neighbor_in_order =
(!next_neighbor.IsValid() || !next_neighbor.LessThan(old_ordinal));
// Still use the old position if it maintains the order with both neighbors.
if (is_prev_neighbor_in_order && is_next_neighbor_in_order) {
*target_position = old_ordinal;
return true;
}
}
*target_position =
CalculatePositionBetweenNeighbors(prev_neighbor, next_neighbor);
return true;
}
} // namespace
std::vector<reorder::ReorderParam> GenerateReorderParamsForSyncItems(
ash::AppListSortOrder order,
const AppListSyncableService::SyncItemMap& sync_item_map) {
DCHECK_GT(sync_item_map.size(), 1u);
switch (order) {
case ash::AppListSortOrder::kNameAlphabetical:
case ash::AppListSortOrder::kNameReverseAlphabetical: {
std::vector<reorder::SyncItemWrapper<std::u16string>> wrappers =
reorder::GenerateWrappersFromSyncItems<std::u16string>(sync_item_map);
return GenerateReorderParamsImpl(order, &wrappers);
}
case ash::AppListSortOrder::kColor: {
std::vector<reorder::SyncItemWrapper<ash::IconColor>> wrappers =
reorder::GenerateWrappersFromSyncItems<ash::IconColor>(sync_item_map);
return GenerateReorderParamsImpl(order, &wrappers);
}
case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst: {
std::vector<reorder::SyncItemWrapper<EphemeralAwareName>> wrappers =
reorder::GenerateWrappersFromSyncItems<EphemeralAwareName>(
sync_item_map);
return GenerateReorderParamsImpl(order, &wrappers);
}
case ash::AppListSortOrder::kCustom:
NOTREACHED_IN_MIGRATION();
return std::vector<reorder::ReorderParam>();
}
}
std::vector<reorder::ReorderParam> GenerateReorderParamsForAppListItems(
ash::AppListSortOrder order,
const std::vector<const ChromeAppListItem*>& app_list_items) {
DCHECK_GT(app_list_items.size(), 1u);
switch (order) {
case ash::AppListSortOrder::kNameAlphabetical:
case ash::AppListSortOrder::kNameReverseAlphabetical: {
std::vector<reorder::SyncItemWrapper<std::u16string>> wrappers =
reorder::GenerateWrappersFromAppListItems<std::u16string>(
app_list_items, /*ignored_id=*/std::nullopt);
return GenerateReorderParamsImpl(order, &wrappers);
}
case ash::AppListSortOrder::kColor: {
std::vector<reorder::SyncItemWrapper<ash::IconColor>> wrappers =
reorder::GenerateWrappersFromAppListItems<ash::IconColor>(
app_list_items, /*ignored_id=*/std::nullopt);
return GenerateReorderParamsImpl(order, &wrappers);
}
case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst: {
std::vector<reorder::SyncItemWrapper<EphemeralAwareName>> wrappers =
reorder::GenerateWrappersFromAppListItems<EphemeralAwareName>(
app_list_items, /*ignored_id=*/std::nullopt);
return GenerateReorderParamsImpl(order, &wrappers);
}
case ash::AppListSortOrder::kCustom:
NOTREACHED_IN_MIGRATION();
return std::vector<reorder::ReorderParam>();
}
}
bool CalculateItemPositionInOrder(
ash::AppListSortOrder order,
const ash::AppListItemMetadata& metadata,
const std::vector<const ChromeAppListItem*>& local_items,
const AppListSyncableService::SyncItemMap* global_items,
syncer::StringOrdinal* target_position) {
switch (order) {
case ash::AppListSortOrder::kCustom:
// Insert `item` at the front when the sort order is kCustom.
DCHECK(global_items);
*target_position = CalculateFrontPosition(*global_items);
return true;
case ash::AppListSortOrder::kNameAlphabetical:
case ash::AppListSortOrder::kNameReverseAlphabetical: {
UErrorCode error = U_ZERO_ERROR;
std::unique_ptr<icu::Collator> collator(
icu::Collator::createInstance(error));
if (U_FAILURE(error))
collator.reset();
StringWrapperComparator comparator(IsIncreasingOrder(order),
collator.get());
return CalculatePositionForSyncItemWrapper(
order, reorder::SyncItemWrapper<std::u16string>(metadata),
local_items, global_items, comparator, target_position);
}
case ash::AppListSortOrder::kColor: {
IconColorWrapperComparator comparator;
return CalculatePositionForSyncItemWrapper(
order, reorder::SyncItemWrapper<ash::IconColor>(metadata),
local_items, global_items, comparator, target_position);
}
case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst: {
UErrorCode error = U_ZERO_ERROR;
std::unique_ptr<icu::Collator> collator(
icu::Collator::createInstance(error));
if (U_FAILURE(error))
collator.reset();
EphemeralStateAndNameComparator comparator(collator.get());
return CalculatePositionForSyncItemWrapper(
order, reorder::SyncItemWrapper<EphemeralAwareName>(metadata),
local_items, global_items, comparator, target_position);
}
}
}
syncer::StringOrdinal CalculateFrontPosition(
const AppListSyncableService::SyncItemMap& sync_item_map) {
syncer::StringOrdinal minimum_valid_ordinal;
for (auto iter = sync_item_map.cbegin(); iter != sync_item_map.cend();
++iter) {
const syncer::StringOrdinal& ordinal = iter->second->item_ordinal;
// `ordinal` may be invalid (especially in tests).
if (!ordinal.IsValid())
continue;
if (!minimum_valid_ordinal.IsValid() ||
ordinal.LessThan(minimum_valid_ordinal)) {
minimum_valid_ordinal = ordinal;
}
}
if (minimum_valid_ordinal.IsValid())
return minimum_valid_ordinal.CreateBefore();
return syncer::StringOrdinal::CreateInitialOrdinal();
}
float CalculateEntropyForTest(ash::AppListSortOrder order,
AppListModelUpdater* model_updater) {
switch (order) {
case ash::AppListSortOrder::kCustom:
case ash::AppListSortOrder::kColor:
case ash::AppListSortOrder::kAlphabeticalEphemeralAppFirst:
NOTREACHED_IN_MIGRATION();
return 0.f;
case ash::AppListSortOrder::kNameAlphabetical:
case ash::AppListSortOrder::kNameReverseAlphabetical:
std::vector<reorder::SyncItemWrapper<std::u16string>>
local_item_wrappers =
reorder::GenerateWrappersFromAppListItems<std::u16string>(
model_updater->GetItems(), /*ignored_id=*/std::nullopt);
float entropy = 0.f;
CalculateEntropyAndGetSortedSubsequence(
order, &local_item_wrappers, &entropy,
static_cast<std::vector<reorder::SyncItemWrapper<std::u16string>>*>(
nullptr));
return entropy;
}
}
} // namespace reorder
} // namespace app_list