| // 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.h" |
| |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/notreached.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/uuid.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 "ui/gfx/image/image.h" |
| #include "url/gurl.h" |
| |
| SavedTabGroup::SavedTabGroup( |
| const std::u16string& title, |
| const tab_groups::TabGroupColorId& color, |
| const std::vector<SavedTabGroupTab>& urls, |
| std::optional<size_t> position, |
| std::optional<base::Uuid> saved_guid, |
| std::optional<tab_groups::TabGroupId> local_group_id, |
| std::optional<base::Time> creation_time_windows_epoch_micros, |
| std::optional<base::Time> update_time_windows_epoch_micros) |
| : saved_guid_(saved_guid.value_or(base::Uuid::GenerateRandomV4())), |
| local_group_id_(local_group_id), |
| title_(title), |
| color_(color), |
| saved_tabs_(urls), |
| position_(position), |
| creation_time_windows_epoch_micros_( |
| creation_time_windows_epoch_micros.value_or(base::Time::Now())), |
| update_time_windows_epoch_micros_( |
| update_time_windows_epoch_micros.value_or(base::Time::Now())) {} |
| |
| SavedTabGroup::SavedTabGroup(const SavedTabGroup& other) = default; |
| |
| SavedTabGroup::~SavedTabGroup() = default; |
| |
| const SavedTabGroupTab* SavedTabGroup::GetTab( |
| const base::Uuid& saved_tab_guid) const { |
| std::optional<int> index = GetIndexOfTab(saved_tab_guid); |
| if (!index.has_value()) |
| return nullptr; |
| return &saved_tabs()[index.value()]; |
| } |
| |
| const SavedTabGroupTab* SavedTabGroup::GetTab( |
| const base::Token& local_tab_id) const { |
| std::optional<int> index = GetIndexOfTab(local_tab_id); |
| if (!index.has_value()) |
| return nullptr; |
| return &saved_tabs()[index.value()]; |
| } |
| |
| SavedTabGroupTab* SavedTabGroup::GetTab(const base::Uuid& saved_tab_guid) { |
| std::optional<int> index = GetIndexOfTab(saved_tab_guid); |
| if (!index.has_value()) { |
| return nullptr; |
| } |
| return &saved_tabs()[index.value()]; |
| } |
| |
| SavedTabGroupTab* SavedTabGroup::GetTab(const base::Token& local_tab_id) { |
| std::optional<int> index = GetIndexOfTab(local_tab_id); |
| if (!index.has_value()) { |
| return nullptr; |
| } |
| return &saved_tabs()[index.value()]; |
| } |
| |
| bool SavedTabGroup::ContainsTab(const base::Uuid& saved_tab_guid) const { |
| std::optional<int> index = GetIndexOfTab(saved_tab_guid); |
| return index.has_value(); |
| } |
| |
| bool SavedTabGroup::ContainsTab(const base::Token& local_tab_id) const { |
| std::optional<int> index = GetIndexOfTab(local_tab_id); |
| return index.has_value(); |
| } |
| |
| std::optional<int> SavedTabGroup::GetIndexOfTab( |
| const base::Uuid& saved_tab_guid) const { |
| auto it = base::ranges::find_if( |
| saved_tabs(), [saved_tab_guid](const SavedTabGroupTab& tab) { |
| return tab.saved_tab_guid() == saved_tab_guid; |
| }); |
| if (it != saved_tabs().end()) |
| return it - saved_tabs().begin(); |
| return std::nullopt; |
| } |
| |
| std::optional<int> SavedTabGroup::GetIndexOfTab( |
| const base::Token& local_tab_id) const { |
| auto it = base::ranges::find_if(saved_tabs(), |
| [local_tab_id](const SavedTabGroupTab& tab) { |
| return tab.local_tab_id() == local_tab_id; |
| }); |
| if (it != saved_tabs().end()) |
| return it - saved_tabs().begin(); |
| return std::nullopt; |
| } |
| |
| SavedTabGroup& SavedTabGroup::SetTitle(std::u16string title) { |
| title_ = title; |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::SetColor(tab_groups::TabGroupColorId color) { |
| color_ = color; |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::SetLocalGroupId( |
| std::optional<tab_groups::TabGroupId> tab_group_id) { |
| local_group_id_ = tab_group_id; |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::SetUpdateTimeWindowsEpochMicros( |
| base::Time update_time_windows_epoch_micros) { |
| update_time_windows_epoch_micros_ = update_time_windows_epoch_micros; |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::SetPosition(size_t position) { |
| position_ = position; |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::AddTabLocally(SavedTabGroupTab tab) { |
| InsertTabImpl(tab); |
| UpdateTabPositionsImpl(); |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::AddTabFromSync(SavedTabGroupTab tab) { |
| InsertTabImpl(tab); |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::RemoveTabLocally( |
| const base::Uuid& saved_tab_guid) { |
| RemoveTabImpl(saved_tab_guid); |
| UpdateTabPositionsImpl(); |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::RemoveTabFromSync( |
| const base::Uuid& saved_tab_guid) { |
| RemoveTabImpl(saved_tab_guid); |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::UpdateTab(SavedTabGroupTab tab) { |
| std::optional<size_t> index = GetIndexOfTab(tab.saved_tab_guid()); |
| CHECK(index.has_value()); |
| CHECK_GE(index.value(), 0u); |
| CHECK_LT(index.value(), saved_tabs_.size()); |
| saved_tabs_.erase(saved_tabs_.begin() + index.value()); |
| saved_tabs_.insert(saved_tabs_.begin() + index.value(), std::move(tab)); |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::ReplaceTabAt(const base::Uuid& tab_id, |
| SavedTabGroupTab tab) { |
| std::optional<size_t> index = GetIndexOfTab(tab_id); |
| CHECK(index.has_value()); |
| CHECK_GE(index.value(), 0u); |
| CHECK_LT(index.value(), saved_tabs_.size()); |
| saved_tabs_.erase(saved_tabs_.begin() + index.value()); |
| saved_tabs_.insert(saved_tabs_.begin() + index.value(), std::move(tab)); |
| UpdateTabPositionsImpl(); |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::MoveTabLocally(const base::Uuid& saved_tab_guid, |
| size_t new_index) { |
| MoveTabImpl(saved_tab_guid, new_index); |
| UpdateTabPositionsImpl(); |
| return *this; |
| } |
| |
| SavedTabGroup& SavedTabGroup::MoveTabFromSync(const base::Uuid& saved_tab_guid, |
| size_t new_index) { |
| MoveTabImpl(saved_tab_guid, new_index); |
| return *this; |
| } |
| |
| void SavedTabGroup::MoveTabImpl(const base::Uuid& saved_tab_guid, |
| size_t new_index) { |
| std::optional<size_t> curr_index = GetIndexOfTab(saved_tab_guid); |
| CHECK(curr_index.has_value()); |
| CHECK_GE(curr_index.value(), 0u); |
| CHECK_LT(curr_index.value(), saved_tabs_.size()); |
| CHECK_GE(new_index, 0u); |
| CHECK_LT(new_index, saved_tabs_.size()); |
| |
| if (curr_index > new_index) { |
| std::rotate(saved_tabs_.begin() + new_index, |
| saved_tabs_.begin() + curr_index.value(), |
| saved_tabs_.begin() + curr_index.value() + 1); |
| } else if (curr_index < new_index) { |
| std::rotate( |
| saved_tabs_.rbegin() + ((saved_tabs_.size() - 1) - new_index), |
| saved_tabs_.rbegin() + ((saved_tabs_.size() - 1) - curr_index.value()), |
| saved_tabs_.rbegin() + ((saved_tabs_.size() - 1) - curr_index.value()) + |
| 1); |
| } |
| } |
| |
| void SavedTabGroup::InsertTabImpl(SavedTabGroupTab tab) { |
| CHECK(!ContainsTab(tab.saved_tab_guid())); |
| |
| if (!tab.position().has_value()) { |
| tab.SetPosition(saved_tabs_.size()); |
| } |
| |
| // We can always safely insert the first tab at the end. We can also safely |
| // insert `tab` if its position is larger than the position at the end of |
| // `saved_tabs_`. |
| if (saved_tabs_.empty() || |
| saved_tabs_[saved_tabs_.size() - 1].position() < tab.position()) { |
| saved_tabs_.emplace_back(std::move(tab)); |
| return; |
| } |
| |
| // Insert `tab` in front of an element if one of these criteria |
| // are met: |
| // 1. The current index is larger than `tab`. |
| // 2. The current index has the same position as `tab` and is not |
| // the most recently updated position. |
| for (size_t index = 0; index < saved_tabs_.size(); ++index) { |
| const SavedTabGroupTab& curr_tab = saved_tabs_[index]; |
| bool curr_position_larger = curr_tab.position() > tab.position(); |
| bool curr_position_same = curr_tab.position() == tab.position(); |
| bool curr_position_least_recently_updated = |
| curr_tab.update_time_windows_epoch_micros() < |
| tab.update_time_windows_epoch_micros(); |
| |
| if (curr_position_larger || |
| (curr_position_same && curr_position_least_recently_updated)) { |
| saved_tabs_.insert(saved_tabs_.begin() + index, std::move(tab)); |
| return; |
| } |
| } |
| |
| // This can happen when the last element of the vector has the same position |
| // as `group` and was more recently updated. |
| saved_tabs_.push_back(std::move(tab)); |
| } |
| |
| void SavedTabGroup::UpdateTabPositionsImpl() { |
| for (size_t i = 0; i < saved_tabs_.size(); ++i) { |
| saved_tabs_[i].SetPosition(i); |
| } |
| |
| SetUpdateTimeWindowsEpochMicros(base::Time::Now()); |
| } |
| |
| bool SavedTabGroup::ShouldMergeGroup( |
| const sync_pb::SavedTabGroupSpecifics& sync_specific) const { |
| bool sync_update_is_latest = |
| sync_specific.update_time_windows_epoch_micros() >= |
| update_time_windows_epoch_micros() |
| .ToDeltaSinceWindowsEpoch() |
| .InMicroseconds(); |
| // TODO(dljames): crbug/1371953 - Investigate if we should consider the |
| // creation time. |
| return sync_update_is_latest; |
| } |
| |
| std::unique_ptr<sync_pb::SavedTabGroupSpecifics> SavedTabGroup::MergeGroup( |
| const sync_pb::SavedTabGroupSpecifics& sync_specific) { |
| if (ShouldMergeGroup(sync_specific)) { |
| SetTitle(base::UTF8ToUTF16(sync_specific.group().title())); |
| SetColor(SyncColorToTabGroupColor(sync_specific.group().color())); |
| SetPosition(sync_specific.group().position()); |
| SetUpdateTimeWindowsEpochMicros(base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(sync_specific.update_time_windows_epoch_micros()))); |
| } |
| |
| return ToSpecifics(); |
| } |
| |
| // static |
| SavedTabGroup SavedTabGroup::FromSpecifics( |
| const sync_pb::SavedTabGroupSpecifics& specific) { |
| const tab_groups::TabGroupColorId color = |
| SyncColorToTabGroupColor(specific.group().color()); |
| const std::u16string& title = base::UTF8ToUTF16(specific.group().title()); |
| const size_t position = specific.group().position(); |
| |
| const base::Uuid guid = base::Uuid::ParseLowercase(specific.guid()); |
| const base::Time creation_time = base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(specific.creation_time_windows_epoch_micros())); |
| const base::Time update_time = base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(specific.update_time_windows_epoch_micros())); |
| SavedTabGroup group = SavedTabGroup(title, color, {}, position, guid, |
| std::nullopt, creation_time); |
| group.SetUpdateTimeWindowsEpochMicros(update_time); |
| |
| return group; |
| } |
| |
| std::unique_ptr<sync_pb::SavedTabGroupSpecifics> SavedTabGroup::ToSpecifics() |
| const { |
| std::unique_ptr<sync_pb::SavedTabGroupSpecifics> pb_specific = |
| std::make_unique<sync_pb::SavedTabGroupSpecifics>(); |
| pb_specific->set_guid(saved_guid().AsLowercaseString()); |
| pb_specific->set_creation_time_windows_epoch_micros( |
| creation_time_windows_epoch_micros() |
| .ToDeltaSinceWindowsEpoch() |
| .InMicroseconds()); |
| pb_specific->set_update_time_windows_epoch_micros( |
| update_time_windows_epoch_micros() |
| .ToDeltaSinceWindowsEpoch() |
| .InMicroseconds()); |
| |
| sync_pb::SavedTabGroup* pb_group = pb_specific->mutable_group(); |
| pb_group->set_color(TabGroupColorToSyncColor(color())); |
| pb_group->set_title(base::UTF16ToUTF8(title())); |
| pb_group->set_position(position().value()); |
| // Note: When adding a new syncable field, also update IsSyncEquivalent(). |
| |
| return pb_specific; |
| } |
| |
| bool SavedTabGroup::IsSyncEquivalent(const SavedTabGroup& other) const { |
| return saved_guid() == other.saved_guid() && color() == other.color() && |
| title() == other.title() && position() == other.position(); |
| } |
| |
| // static |
| tab_groups::TabGroupColorId SavedTabGroup::SyncColorToTabGroupColor( |
| const sync_pb::SavedTabGroup::SavedTabGroupColor color) { |
| switch (color) { |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_GREY: |
| return tab_groups::TabGroupColorId::kGrey; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_BLUE: |
| return tab_groups::TabGroupColorId::kBlue; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_RED: |
| return tab_groups::TabGroupColorId::kRed; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_YELLOW: |
| return tab_groups::TabGroupColorId::kYellow; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_GREEN: |
| return tab_groups::TabGroupColorId::kGreen; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_PINK: |
| return tab_groups::TabGroupColorId::kPink; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_PURPLE: |
| return tab_groups::TabGroupColorId::kPurple; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_CYAN: |
| return tab_groups::TabGroupColorId::kCyan; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_ORANGE: |
| return tab_groups::TabGroupColorId::kOrange; |
| case sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_UNSPECIFIED: |
| return tab_groups::TabGroupColorId::kGrey; |
| } |
| } |
| |
| // static |
| sync_pb::SavedTabGroup_SavedTabGroupColor |
| SavedTabGroup::TabGroupColorToSyncColor( |
| const tab_groups::TabGroupColorId color) { |
| switch (color) { |
| case tab_groups::TabGroupColorId::kGrey: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_GREY; |
| case tab_groups::TabGroupColorId::kBlue: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_BLUE; |
| case tab_groups::TabGroupColorId::kRed: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_RED; |
| case tab_groups::TabGroupColorId::kYellow: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_YELLOW; |
| case tab_groups::TabGroupColorId::kGreen: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_GREEN; |
| case tab_groups::TabGroupColorId::kPink: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_PINK; |
| case tab_groups::TabGroupColorId::kPurple: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_PURPLE; |
| case tab_groups::TabGroupColorId::kCyan: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_CYAN; |
| case tab_groups::TabGroupColorId::kOrange: |
| return sync_pb::SavedTabGroup::SAVED_TAB_GROUP_COLOR_ORANGE; |
| } |
| |
| NOTREACHED() << "No known conversion for the supplied color."; |
| } |
| |
| void SavedTabGroup::RemoveTabImpl(const base::Uuid& saved_tab_guid) { |
| std::optional<size_t> index = GetIndexOfTab(saved_tab_guid); |
| CHECK(index.has_value()); |
| CHECK_GE(index.value(), 0u); |
| CHECK_LT(index.value(), saved_tabs_.size()); |
| saved_tabs_.erase(saved_tabs_.begin() + index.value()); |
| } |