// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/app_list/model/app_list_item_list.h"
#include <utility>
#include "ash/app_list/model/app_list_item.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_model_delegate.h"
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
namespace ash {
AppListItemList::AppListItemList(AppListModelDelegate* app_list_model_delegate)
: app_list_model_delegate_(app_list_model_delegate) {}
AppListItemList::~AppListItemList() = default;
void AppListItemList::AddObserver(AppListItemListObserver* observer) {
observers_.AddObserver(observer);
}
void AppListItemList::RemoveObserver(AppListItemListObserver* observer) {
DCHECK(observers_.HasObserver(observer));
observers_.RemoveObserver(observer);
}
AppListItem* AppListItemList::FindItem(const std::string& id) {
for (const auto& item : app_list_items_) {
if (item->id() == id)
return item.get();
}
return nullptr;
}
// TODO(crbug.com/40593633): Make it return iterator to avoid unnecessary
// check in this code.
bool AppListItemList::FindItemIndex(const std::string& id, size_t* index) {
for (size_t i = 0; i < app_list_items_.size(); ++i) {
if (app_list_items_[i]->id() == id) {
*index = i;
return true;
}
}
return false;
}
void AppListItemList::MoveItem(size_t from_index, size_t to_index) {
// TODO(https://crbug.com/1257779): this function triggers updates in item
// positions from ash so this function should be moved to
// `AppListModelDelegate`.
DCHECK_LT(from_index, item_count());
// Speculative fix for crash, possibly due to single-item folders
// (see https://crbug.com/937431).
// A folder could have single item due to the following reasons:
// (1) The folder is allowed to contain only one item. Or
// (2) The app list sync is in progress. For example, when the app list is
// syncing two apps under the same folder, one app could be added to the
// folder before the other by a noticeable time interval. As a result, the
// folder contains one item temporarily.
if (item_count() <= 1)
return;
// Speculative fix for crash, possibly due |to_index| == item_count().
// Make |to_index| point to the last item. https://crbug.com/1166011
if (to_index >= item_count()) {
DCHECK_GT(item_count(), 1u);
to_index = item_count() - 1;
}
if (from_index == to_index)
return;
AppListItem* target_item = app_list_items_[from_index].get();
DVLOG(2) << "MoveItem: " << from_index << " -> " << to_index << " ["
<< target_item->position().ToDebugString() << "]";
if (from_index < to_index) {
// Calculate as if the item at `from_index` is removed from the list.
++to_index;
}
// Update the position
AppListItem* prev = to_index > 0 ? item_at(to_index - 1) : nullptr;
AppListItem* next = to_index < item_count() ? item_at(to_index) : nullptr;
CHECK_NE(prev, next);
syncer::StringOrdinal new_position;
if (!prev) {
new_position = next->position().CreateBefore();
} else if (!next) {
new_position = prev->position().CreateAfter();
} else {
// It is possible that items were added with the same ordinal. To
// successfully move the item we need to fix this. We do not try to fix this
// when an item is added in order to avoid possible edge cases with sync.
if (prev->position().Equals(next->position()))
FixItemPosition(to_index);
new_position = prev->position().CreateBetween(next->position());
}
DVLOG(2) << "Move: "
<< " Prev: " << (prev ? prev->position().ToDebugString() : "(none)")
<< " Next: " << (next ? next->position().ToDebugString() : "(none)")
<< " -> " << new_position.ToDebugString();
// Update app list items through a delegate so that the browser side always
// updates app list items before the ash side.
app_list_model_delegate_->RequestPositionUpdate(
target_item->id(), new_position, RequestPositionUpdateReason::kMoveItem);
}
bool AppListItemList::SetItemPosition(AppListItem* item,
syncer::StringOrdinal new_position) {
DCHECK(item);
size_t from_index;
if (!FindItemIndex(item->id(), &from_index)) {
LOG(ERROR) << "SetItemPosition: Not in list: " << item->id().substr(0, 8);
return false;
}
DCHECK(item_at(from_index) == item);
if (!new_position.IsValid()) {
size_t last_index = app_list_items_.size() - 1;
if (from_index == last_index)
return false; // Already last item, do nothing.
new_position = item_at(last_index)->position().CreateAfter();
}
// First check if the order would remain the same, in which case just update
// the position.
size_t to_index = GetItemSortOrderIndex(new_position, item->id());
if (to_index == from_index) {
DVLOG(2) << "SetItemPosition: No change: " << item->id().substr(0, 8);
item->set_position(new_position);
return false;
}
// Remove the item and get the updated to index.
auto target_item = std::move(app_list_items_[from_index]);
app_list_items_.erase(app_list_items_.begin() + from_index);
to_index = GetItemSortOrderIndex(new_position, target_item->id());
DVLOG(2) << "SetItemPosition: " << target_item->id().substr(0, 8) << " -> "
<< new_position.ToDebugString() << " From: " << from_index
<< " To: " << to_index;
target_item->set_position(new_position);
app_list_items_.insert(app_list_items_.begin() + to_index,
std::move(target_item));
for (auto& observer : observers_)
observer.OnListItemMoved(from_index, to_index, item);
return true;
}
std::string AppListItemList::ToString() {
std::string out;
for (size_t i = 0; i < app_list_items_.size(); ++i) {
out.append(base::NumberToString(i));
out.append(": ");
out.append(app_list_items_[i]->id());
out.append("\n");
}
return out;
}
// AppListItemList private
syncer::StringOrdinal AppListItemList::CreatePositionBefore(
const syncer::StringOrdinal& position) {
if (app_list_items_.empty())
return syncer::StringOrdinal::CreateInitialOrdinal();
size_t nitems = app_list_items_.size();
size_t index;
if (!position.IsValid()) {
index = nitems;
} else {
for (index = 0; index < nitems; ++index) {
if (!item_at(index)->position().LessThan(position))
break;
}
}
if (index == 0)
return item_at(0)->position().CreateBefore();
if (index == nitems)
return item_at(nitems - 1)->position().CreateAfter();
return item_at(index - 1)->position().CreateBetween(
item_at(index)->position());
}
AppListItem* AppListItemList::AddItem(std::unique_ptr<AppListItem> item_ptr) {
AppListItem* item = item_ptr.get();
CHECK(!base::Contains(app_list_items_, item,
&std::unique_ptr<AppListItem>::get));
EnsureValidItemPosition(item);
size_t index = GetItemSortOrderIndex(item->position(), item->id());
app_list_items_.insert(app_list_items_.begin() + index, std::move(item_ptr));
for (auto& observer : observers_)
observer.OnListItemAdded(index, item);
return item;
}
void AppListItemList::DeleteItem(const std::string& id) {
std::unique_ptr<AppListItem> item = RemoveItem(id);
// |item| will be deleted on destruction.
}
std::unique_ptr<AppListItem> AppListItemList::RemoveItem(
const std::string& id) {
size_t index;
if (!FindItemIndex(id, &index))
LOG(FATAL) << "RemoveItem: Not found: " << id;
return RemoveItemAt(index);
}
std::unique_ptr<AppListItem> AppListItemList::RemoveItemAt(size_t index) {
CHECK_LT(index, item_count());
auto item = std::move(app_list_items_[index]);
app_list_items_.erase(app_list_items_.begin() + index);
for (auto& observer : observers_)
observer.OnListItemRemoved(index, item.get());
return item;
}
void AppListItemList::DeleteItemAt(size_t index) {
std::unique_ptr<AppListItem> item = RemoveItemAt(index);
// |item| will be deleted on destruction.
}
void AppListItemList::EnsureValidItemPosition(AppListItem* item) {
syncer::StringOrdinal position = item->position();
if (position.IsValid())
return;
size_t nitems = app_list_items_.size();
if (nitems == 0) {
position = syncer::StringOrdinal::CreateInitialOrdinal();
} else {
position = item_at(nitems - 1)->position().CreateAfter();
}
item->set_position(position);
}
size_t AppListItemList::GetItemSortOrderIndex(
const syncer::StringOrdinal& position,
const std::string& id) {
DCHECK(position.IsValid());
for (size_t index = 0; index < app_list_items_.size(); ++index) {
if (position.LessThan(item_at(index)->position()) ||
(position.Equals(item_at(index)->position()) &&
(id < item_at(index)->id()))) {
return index;
}
}
return app_list_items_.size();
}
void AppListItemList::FixItemPosition(size_t index) {
// TODO(https://crbug.com/1257779): this function triggers updates in item
// positions from ash so this function should be moved to
// `AppListModelDelegate`.
DVLOG(1) << "FixItemPosition: " << index;
size_t nitems = item_count();
DCHECK_LT(index, nitems);
DCHECK_GT(index, 0u);
// Update the position of |index| and any necessary subsequent items.
// First, find the next item that has a different position.
const syncer::StringOrdinal duplicate_position =
item_at(index - 1)->position();
size_t last_index = index + 1;
for (; last_index < nitems; ++last_index) {
if (!item_at(last_index)->position().Equals(duplicate_position))
break;
}
// Store the pairs of ids and new positions before requesting to update
// positions. Because position update may result in item list reorder.
std::vector<std::pair<std::string, syncer::StringOrdinal>> id_position_pairs;
syncer::StringOrdinal new_position = duplicate_position;
AppListItem* last = last_index < nitems ? item_at(last_index) : nullptr;
for (size_t i = index; i < last_index; ++i) {
new_position = last ? new_position.CreateBetween(last->position())
: new_position.CreateAfter();
id_position_pairs.emplace_back(item_at(i)->id(), new_position);
}
for (const auto& pair : id_position_pairs) {
app_list_model_delegate_->RequestPositionUpdate(
pair.first, pair.second, RequestPositionUpdateReason::kFixItem);
}
}
} // namespace ash