blob: 93ed90af85ff4dd5be473a790f64ebc1cb6e85f3 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/saved_tab_groups/saved_tab_group_model.h"
#include <cstddef>
#include <memory>
#include <vector>
#include "base/guid.h"
#include "base/observer_list.h"
#include "base/strings/utf_string_conversions.h"
#include "components/saved_tab_groups/saved_tab_group.h"
#include "components/saved_tab_groups/saved_tab_group_model_observer.h"
#include "components/saved_tab_groups/saved_tab_group_tab.h"
#include "components/sync/protocol/saved_tab_group_specifics.pb.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
SavedTabGroupModel::SavedTabGroupModel() = default;
SavedTabGroupModel::~SavedTabGroupModel() = default;
absl::optional<int> SavedTabGroupModel::GetIndexOf(
tab_groups::TabGroupId tab_group_id) const {
for (size_t i = 0; i < saved_tab_groups_.size(); i++) {
if (saved_tab_groups_[i].local_group_id() == tab_group_id)
return i;
}
return absl::nullopt;
}
absl::optional<int> SavedTabGroupModel::GetIndexOf(const base::GUID& id) const {
for (size_t i = 0; i < saved_tab_groups_.size(); i++) {
if (saved_tab_groups_[i].saved_guid() == id)
return i;
}
return absl::nullopt;
}
const SavedTabGroup* SavedTabGroupModel::Get(const base::GUID& id) const {
absl::optional<int> index = GetIndexOf(id);
if (!index.has_value()) {
return nullptr;
}
return &saved_tab_groups_[index.value()];
}
const SavedTabGroup* SavedTabGroupModel::Get(
const tab_groups::TabGroupId local_group_id) const {
absl::optional<int> index = GetIndexOf(local_group_id);
if (!index.has_value())
return nullptr;
return &saved_tab_groups_[index.value()];
}
void SavedTabGroupModel::Add(SavedTabGroup saved_group) {
base::GUID group_guid = saved_group.saved_guid();
CHECK(!Contains(group_guid));
// Give a default position to groups if it is not already set.
if (saved_group.position() == SavedTabGroup::kUnsetPosition)
saved_group.SetPosition(Count());
InsertGroupImpl(std::move(saved_group));
for (auto& observer : observers_) {
observer.SavedTabGroupAddedLocally(Get(group_guid)->saved_guid());
}
}
void SavedTabGroupModel::Remove(const tab_groups::TabGroupId tab_group_id) {
if (!Contains(tab_group_id))
return;
const int index = GetIndexOf(tab_group_id).value();
base::GUID removed_guid = Get(tab_group_id)->saved_guid();
std::unique_ptr<SavedTabGroup> removed_group = RemoveImpl(index);
UpdateGroupPositionsImpl();
for (auto& observer : observers_) {
observer.SavedTabGroupRemovedLocally(removed_group.get());
}
}
void SavedTabGroupModel::Remove(const base::GUID& id) {
if (!Contains(id))
return;
const int index = GetIndexOf(id).value();
base::GUID removed_guid = Get(id)->saved_guid();
std::unique_ptr<SavedTabGroup> removed_group = RemoveImpl(index);
UpdateGroupPositionsImpl();
for (auto& observer : observers_) {
observer.SavedTabGroupRemovedLocally(removed_group.get());
}
}
void SavedTabGroupModel::UpdateVisualData(
tab_groups::TabGroupId tab_group_id,
const tab_groups::TabGroupVisualData* visual_data) {
if (!Contains(tab_group_id))
return;
const absl::optional<int> index = GetIndexOf(tab_group_id);
UpdateVisualDataImpl(index.value(), visual_data);
base::GUID updated_guid = Get(tab_group_id)->saved_guid();
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(updated_guid);
}
}
void SavedTabGroupModel::UpdateVisualData(
const base::GUID& id,
const tab_groups::TabGroupVisualData* visual_data) {
if (!Contains(id))
return;
const absl::optional<int> index = GetIndexOf(id);
UpdateVisualDataImpl(index.value(), visual_data);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(id);
}
}
void SavedTabGroupModel::AddedFromSync(SavedTabGroup saved_group) {
base::GUID group_guid = saved_group.saved_guid();
if (Contains(group_guid))
return;
InsertGroupImpl(std::move(saved_group));
for (auto& observer : observers_) {
observer.SavedTabGroupAddedFromSync(Get(group_guid)->saved_guid());
}
}
void SavedTabGroupModel::RemovedFromSync(
const tab_groups::TabGroupId tab_group_id) {
if (!Contains(tab_group_id))
return;
const absl::optional<int> index = GetIndexOf(tab_group_id);
base::GUID removed_guid = Get(tab_group_id)->saved_guid();
std::unique_ptr<SavedTabGroup> removed_group = RemoveImpl(index.value());
for (auto& observer : observers_) {
observer.SavedTabGroupRemovedFromSync(removed_group.get());
}
}
void SavedTabGroupModel::RemovedFromSync(const base::GUID& id) {
if (!Contains(id))
return;
const absl::optional<int> index = GetIndexOf(id);
base::GUID removed_guid = Get(id)->saved_guid();
std::unique_ptr<SavedTabGroup> removed_group = RemoveImpl(index.value());
for (auto& observer : observers_) {
observer.SavedTabGroupRemovedFromSync(removed_group.get());
}
}
void SavedTabGroupModel::UpdatedVisualDataFromSync(
tab_groups::TabGroupId tab_group_id,
const tab_groups::TabGroupVisualData* visual_data) {
if (!Contains(tab_group_id))
return;
const absl::optional<int> index = GetIndexOf(tab_group_id);
UpdateVisualDataImpl(index.value(), visual_data);
base::GUID updated_guid = Get(tab_group_id)->saved_guid();
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedFromSync(updated_guid);
}
}
void SavedTabGroupModel::UpdatedVisualDataFromSync(
const base::GUID& id,
const tab_groups::TabGroupVisualData* visual_data) {
if (!Contains(id))
return;
const absl::optional<int> index = GetIndexOf(id);
UpdateVisualDataImpl(index.value(), visual_data);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedFromSync(id);
}
}
SavedTabGroup* SavedTabGroupModel::GetGroupContainingTab(
const base::GUID& saved_tab_guid) {
for (auto& saved_group : saved_tab_groups_) {
if (saved_group.ContainsTab(saved_tab_guid))
return &saved_group;
}
return nullptr;
}
SavedTabGroup* SavedTabGroupModel::GetGroupContainingTab(
const base::Token& local_tab_id) {
for (auto& saved_group : saved_tab_groups_) {
if (saved_group.ContainsTab(local_tab_id))
return &saved_group;
}
return nullptr;
}
void SavedTabGroupModel::AddTabToGroup(const base::GUID& group_id,
SavedTabGroupTab tab,
bool update_tab_positions) {
if (!Contains(group_id))
return;
const base::GUID tab_id = tab.saved_tab_guid();
absl::optional<int> group_index = GetIndexOf(group_id);
saved_tab_groups_[group_index.value()].AddTab(tab, update_tab_positions);
if (!update_tab_positions) {
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedFromSync(group_id);
}
} else {
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(group_id, tab_id);
}
}
}
void SavedTabGroupModel::UpdateTabInGroup(const base::GUID& group_id,
SavedTabGroupTab tab) {
absl::optional<int> group_index = GetIndexOf(group_id);
CHECK(group_index.has_value());
saved_tab_groups_[group_index.value()].UpdateTab(tab);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(group_id);
}
}
void SavedTabGroupModel::RemoveTabFromGroup(const base::GUID& group_id,
const base::GUID& tab_id,
bool update_tab_positions) {
if (!Contains(group_id))
return;
absl::optional<int> index = GetIndexOf(group_id);
SavedTabGroup group = saved_tab_groups_[index.value()];
if (!group.ContainsTab(tab_id)) {
return;
}
// Remove the group from the model if the last tab will be removed from it.
if (group.saved_tabs().size() == 1) {
Remove(group_id);
return;
}
// Copy `tab_id` to prevent uaf when ungrouping a saved tab: crbug/1401965.
const base::GUID copy_tab_id = tab_id;
saved_tab_groups_[index.value()].RemoveTab(tab_id, update_tab_positions);
// TODO(dljames): Update to use SavedTabGroupRemoveLocally and update the API
// to pass a group_id and an optional tab_id.
if (!update_tab_positions) {
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedFromSync(group_id);
}
} else {
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(group_id, copy_tab_id);
}
}
}
void SavedTabGroupModel::ReplaceTabInGroupAt(const base::GUID& group_id,
const base::GUID& tab_id,
SavedTabGroupTab new_tab) {
if (!Contains(group_id))
return;
// Copy `tab_id` to prevent uaf when ungrouping a saved tab: crbug/1401965.
const base::GUID copy_tab_id = tab_id;
const base::GUID guid = new_tab.saved_tab_guid();
absl::optional<int> index = GetIndexOf(group_id);
saved_tab_groups_[index.value()].ReplaceTabAt(tab_id, new_tab);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(group_id, copy_tab_id);
observer.SavedTabGroupUpdatedLocally(group_id, guid);
}
}
void SavedTabGroupModel::MoveTabInGroupTo(const base::GUID& group_id,
const base::GUID& tab_id,
int new_index) {
if (!Contains(group_id))
return;
// Copy `tab_id` to prevent uaf when ungrouping a saved tab: crbug/1401965.
const base::GUID copy_tab_id = tab_id;
absl::optional<int> index = GetIndexOf(group_id);
saved_tab_groups_[index.value()].MoveTab(tab_id, new_index);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(group_id, copy_tab_id);
}
}
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> SavedTabGroupModel::MergeGroup(
const sync_pb::SavedTabGroupSpecifics& sync_specific) {
const base::GUID& group_id = base::GUID::ParseLowercase(sync_specific.guid());
DCHECK(Contains(group_id));
int index = GetIndexOf(group_id).value();
saved_tab_groups_[index].MergeGroup(std::move(sync_specific));
int preferred_index = Get(group_id)->position();
if (index != preferred_index) {
Reorder(group_id, preferred_index);
}
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedFromSync(group_id);
}
return Get(group_id)->ToSpecifics();
}
std::unique_ptr<sync_pb::SavedTabGroupSpecifics> SavedTabGroupModel::MergeTab(
const sync_pb::SavedTabGroupSpecifics& sync_specific) {
const base::GUID& group_guid =
base::GUID::ParseLowercase(sync_specific.tab().group_guid());
absl::optional<int> group_index = GetIndexOf(group_guid);
CHECK(group_index.has_value());
const base::GUID& tab_guid = base::GUID::ParseLowercase(sync_specific.guid());
CHECK(saved_tab_groups_[group_index.value()].ContainsTab(tab_guid));
SavedTabGroupTab merged_tab(*Get(group_guid)->GetTab(tab_guid));
merged_tab.MergeTab(std::move(sync_specific));
saved_tab_groups_[group_index.value()].ReplaceTabAt(tab_guid, merged_tab);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedFromSync(group_guid,
merged_tab.saved_group_guid());
}
return merged_tab.ToSpecifics();
}
void SavedTabGroupModel::Reorder(const base::GUID& id, int new_index) {
DCHECK_GE(new_index, 0);
DCHECK_LT(new_index, Count());
absl::optional<int> index = GetIndexOf(id);
CHECK(index.has_value());
CHECK_GE(index.value(), 0);
SavedTabGroup group = saved_tab_groups_[index.value()];
saved_tab_groups_.erase(saved_tab_groups_.begin() + index.value());
saved_tab_groups_.emplace(saved_tab_groups_.begin() + new_index,
std::move(group));
UpdateGroupPositionsImpl();
for (auto& observer : observers_) {
observer.SavedTabGroupReorderedLocally();
}
}
void SavedTabGroupModel::UpdateGroupPositionsImpl() {
for (size_t i = 0; i < saved_tab_groups_.size(); ++i)
saved_tab_groups_[i].SetPosition(i);
}
void SavedTabGroupModel::InsertGroupImpl(const SavedTabGroup& group) {
// We can always safely insert the first group.
if (saved_tab_groups_.empty()) {
saved_tab_groups_.emplace_back(std::move(group));
return;
}
// Because saved_tab_groups_ must be in sorted order, we can immediately place
// the group at the end of the vector if `group` is the largest
// element we have seen yet.
if (saved_tab_groups_[saved_tab_groups_.size() - 1].position() <
group.position()) {
saved_tab_groups_.emplace_back(std::move(group));
return;
}
// Insert `group` in front of an element if one of these criteria
// are met:
// 1. The current index is larger than `group`.
// 2. The current index has the same position as `group` and is not
// the most recently updated position.
for (size_t index = 0; index < saved_tab_groups_.size(); ++index) {
const SavedTabGroup& curr_group = saved_tab_groups_[index];
bool curr_position_larger = curr_group.position() > group.position();
bool curr_position_same = curr_group.position() == group.position();
bool curr_position_least_recently_updated =
curr_group.update_time_windows_epoch_micros() <=
group.update_time_windows_epoch_micros();
if (curr_position_larger ||
(curr_position_same && curr_position_least_recently_updated)) {
saved_tab_groups_.insert(saved_tab_groups_.begin() + index,
std::move(group));
return;
}
}
// This can happen when the last element of the vector has the same position
// as `group` and was more recently updated.
saved_tab_groups_.emplace_back(std::move(group));
}
std::vector<sync_pb::SavedTabGroupSpecifics>
SavedTabGroupModel::LoadStoredEntries(
std::vector<sync_pb::SavedTabGroupSpecifics> entries) {
std::vector<SavedTabGroupTab> tabs;
std::vector<sync_pb::SavedTabGroupSpecifics> tabs_missing_groups;
// `entries` is not ordered such that groups are guaranteed to be
// at the front of the vector. As such, we can run into the case where we
// try to add a tab to a group that does not exist for us yet.
for (sync_pb::SavedTabGroupSpecifics proto : entries) {
if (proto.has_group())
Add(SavedTabGroup::FromSpecifics(proto));
else
tabs.emplace_back(SavedTabGroupTab::FromSpecifics(proto));
}
UpdateGroupPositionsImpl();
for (const SavedTabGroupTab& tab : tabs) {
absl::optional<int> index = GetIndexOf(tab.saved_group_guid());
if (!index.has_value()) {
tabs_missing_groups.emplace_back(std::move(*tab.ToSpecifics()));
} else {
base::GUID group_id = tab.saved_group_guid();
AddTabToGroup(group_id, std::move(tab), /*update_tab_positions=*/false);
}
}
return tabs_missing_groups;
}
void SavedTabGroupModel::OnGroupClosedInTabStrip(
const tab_groups::TabGroupId& tab_group_id) {
const absl::optional<int> index = GetIndexOf(tab_group_id);
if (!index.has_value())
return;
SavedTabGroup& saved_group = saved_tab_groups_[index.value()];
saved_group.SetLocalGroupId(absl::nullopt);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(saved_group.saved_guid());
}
}
void SavedTabGroupModel::OnGroupOpenedInTabStrip(
const base::GUID& id,
const tab_groups::TabGroupId& tab_group_id) {
const absl::optional<int> index = GetIndexOf(id);
CHECK(index.has_value());
CHECK_GE(index.value(), 0);
SavedTabGroup& saved_group = saved_tab_groups_[index.value()];
saved_group.SetLocalGroupId(tab_group_id);
for (auto& observer : observers_) {
observer.SavedTabGroupUpdatedLocally(saved_group.saved_guid());
}
}
std::unique_ptr<SavedTabGroup> SavedTabGroupModel::RemoveImpl(int index) {
CHECK_GE(index, 0);
std::unique_ptr<SavedTabGroup> removed_group =
std::make_unique<SavedTabGroup>(std::move(saved_tab_groups_[index]));
saved_tab_groups_.erase(saved_tab_groups_.begin() + index);
return removed_group;
}
void SavedTabGroupModel::UpdateVisualDataImpl(
int index,
const tab_groups::TabGroupVisualData* visual_data) {
SavedTabGroup& saved_group = saved_tab_groups_[index];
if (saved_group.title() == visual_data->title() &&
saved_group.color() == visual_data->color())
return;
saved_group.SetTitle(visual_data->title());
saved_group.SetColor(visual_data->color());
}
void SavedTabGroupModel::AddObserver(SavedTabGroupModelObserver* observer) {
observers_.AddObserver(observer);
}
void SavedTabGroupModel::RemoveObserver(SavedTabGroupModelObserver* observer) {
observers_.RemoveObserver(observer);
}