blob: 0a01f2a2b6a5bebbbdc44627150224bc8de44071 [file]
// Copyright 2020 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/extensions/api/tab_groups/tab_groups_api.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/strings/pattern.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/tabs/windows_util.h"
#include "chrome/browser/extensions/browser_extension_window_controller.h"
#include "chrome/browser/extensions/chrome_extension_function_details.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/common/extensions/api/tab_groups.h"
#include "chrome/common/extensions/api/tabs.h"
#include "chrome/common/extensions/api/windows.h"
#include "chrome/common/extensions/extension_constants.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 "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
#include "extensions/buildflags/buildflags.h"
#include "ui/gfx/range/range.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/browser/ui/android/tab_model/tab_model_observer.h"
#endif // BUILDFLAG(IS_ANDROID)
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions {
namespace {
constexpr char kUnableToFindTabError[] = "Unable to find tab.";
constexpr char kCannotMoveGroupIntoMiddleOfOtherGroupError[] =
"Cannot move the group to an index that is in the middle of another group.";
constexpr char kCannotMoveGroupIntoMiddleOfPinnedTabsError[] =
"Cannot move the group to an index that is in the middle of pinned tabs.";
// Returns true if a group could be moved into the |target_index| of the given
// |tab_strip|. Sets the |error| string otherwise.
bool IndexSupportsGroupMove(TabListInterface* tab_list,
int target_index,
std::string* error) {
// A group can always be moved to the end of the tabstrip.
if (target_index >= tab_list->GetTabCount() || target_index < 0) {
return true;
}
tabs::TabInterface* target_tab = tab_list->GetTab(target_index);
if (!target_tab) {
*error = kUnableToFindTabError;
return false;
}
if (target_tab->IsPinned()) {
*error = kCannotMoveGroupIntoMiddleOfPinnedTabsError;
return false;
}
std::optional<tab_groups::TabGroupId> target_group = target_tab->GetGroup();
// Get the group to the left of the target, if there is one.
std::optional<tab_groups::TabGroupId> adjacent_group;
if (target_index > 0) {
tabs::TabInterface* adjacent_tab = tab_list->GetTab(target_index - 1);
CHECK(adjacent_tab);
adjacent_group = adjacent_tab->GetGroup();
}
if (target_group.has_value() && target_group == adjacent_group) {
*error = kCannotMoveGroupIntoMiddleOfOtherGroupError;
return false;
}
return true;
}
} // namespace
ExtensionFunction::ResponseAction TabGroupsGetFunction::Run() {
std::optional<api::tab_groups::Get::Params> params =
api::tab_groups::Get::Params::Create(args());
DCHECK(params.has_value());
EXTENSION_FUNCTION_VALIDATE(params);
int group_id = params->group_id;
tab_groups::TabGroupId id = tab_groups::TabGroupId::CreateEmpty();
tab_groups::TabGroupVisualData visual_data;
std::string error;
if (!ExtensionTabUtil::GetGroupById(group_id, browser_context(),
include_incognito_information(), nullptr,
&id, &visual_data, &error)) {
return RespondNow(Error(std::move(error)));
}
DCHECK(!id.is_empty());
return RespondNow(ArgumentList(api::tab_groups::Get::Results::Create(
ExtensionTabUtil::CreateTabGroupObject(id, visual_data))));
}
ExtensionFunction::ResponseAction TabGroupsQueryFunction::Run() {
std::optional<api::tab_groups::Query::Params> params =
api::tab_groups::Query::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
base::ListValue result_list;
Profile* profile = Profile::FromBrowserContext(browser_context());
WindowController* window_controller =
ChromeExtensionFunctionDetails(this).GetCurrentWindowController();
if (!window_controller) {
return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError));
}
BrowserWindowInterface* current_browser =
window_controller->GetBrowserWindowInterface();
if (!current_browser) {
return RespondNow(
Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
}
ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
[&](BrowserWindowInterface* browser_window_interface) {
if (!profile->IsSameOrParent(browser_window_interface->GetProfile())) {
return true;
}
if (!include_incognito_information() &&
profile != browser_window_interface->GetProfile()) {
return true;
}
if (!BrowserExtensionWindowController::From(browser_window_interface)
->IsVisibleToTabsAPIForExtension(
extension(), /*allow_dev_tools_windows=*/false)) {
return true;
}
if (params->query_info.window_id) {
const int window_id = *params->query_info.window_id;
if (window_id >= 0 && window_id != ExtensionTabUtil::GetWindowId(
browser_window_interface)) {
return true;
}
if (window_id == extension_misc::kCurrentWindowId &&
browser_window_interface != current_browser) {
return true;
}
}
if (!ExtensionTabUtil::SupportsTabGroups(browser_window_interface)) {
return true;
}
TabListInterface* tab_list =
TabListInterface::From(browser_window_interface);
if (!tab_list) {
return true;
}
for (const tab_groups::TabGroupId& id : tab_list->ListTabGroups()) {
std::optional<tab_groups::TabGroupVisualData> visual_data =
tab_list->GetTabGroupVisualData(id);
if (!visual_data) {
continue;
}
if (params->query_info.collapsed &&
*params->query_info.collapsed != visual_data->is_collapsed()) {
continue;
}
if (params->query_info.title &&
!base::MatchPattern(
visual_data->title(),
base::UTF8ToUTF16(*params->query_info.title))) {
continue;
}
if (params->query_info.color != api::tab_groups::Color::kNone &&
params->query_info.color !=
ExtensionTabUtil::ColorIdToColor(visual_data->color())) {
continue;
}
if (params->query_info.shared.has_value() &&
ExtensionTabUtil::GetSharedStateOfGroup(id) !=
params->query_info.shared.value()) {
continue;
}
result_list.Append(
ExtensionTabUtil::CreateTabGroupObject(id, *visual_data)
.ToValue());
}
return true;
});
return RespondNow(WithArguments(std::move(result_list)));
}
ExtensionFunction::ResponseAction TabGroupsUpdateFunction::Run() {
std::optional<api::tab_groups::Update::Params> params =
api::tab_groups::Update::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
int group_id = params->group_id;
WindowController* window = nullptr;
tab_groups::TabGroupId id = tab_groups::TabGroupId::CreateEmpty();
tab_groups::TabGroupVisualData visual_data;
std::string error;
if (!ExtensionTabUtil::GetGroupById(group_id, browser_context(),
include_incognito_information(), &window,
&id, &visual_data, &error)) {
return RespondNow(Error(std::move(error)));
}
// Since this is in a tab group, there should not be a prerender tab (with no
// window).
CHECK(window);
DCHECK(!id.is_empty());
bool collapsed = visual_data.is_collapsed();
if (params->update_properties.collapsed) {
collapsed = *params->update_properties.collapsed;
}
tab_groups::TabGroupColorId color = visual_data.color();
if (params->update_properties.color != api::tab_groups::Color::kNone) {
color = ExtensionTabUtil::ColorToColorId(params->update_properties.color);
}
std::u16string title = visual_data.title();
if (params->update_properties.title) {
title = base::UTF8ToUTF16(*params->update_properties.title);
}
if (!ExtensionTabUtil::IsTabStripEditable(*window->profile())) {
return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError));
}
BrowserWindowInterface* browser = window->GetBrowserWindowInterface();
if (!browser) {
return RespondNow(
Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
}
if (!ExtensionTabUtil::SupportsTabGroups(browser)) {
return RespondNow(
Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
}
// Update the visual data.
auto* tab_list = TabListInterface::From(browser);
if (!tab_list) {
return RespondNow(
Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
}
tab_groups::TabGroupVisualData new_visual_data(title, color, collapsed);
tab_list->SetTabGroupVisualData(id, new_visual_data);
if (!has_callback()) {
return RespondNow(NoArguments());
}
return RespondNow(ArgumentList(api::tab_groups::Get::Results::Create(
ExtensionTabUtil::CreateTabGroupObject(id, new_visual_data))));
}
////////////////////////////////////////////////////////////////////////////////
#if BUILDFLAG(IS_ANDROID)
// Helper class to observe for tab group creation notifications. Used on Android
// because cross-window tab group moves are asynchronous.
class TabGroupsMoveFunction::ObserverHelper : public TabModelObserver {
public:
ObserverHelper(TabGroupsMoveFunction* owner, TabModel* tab_model)
: owner_(owner), tab_model_(tab_model) {
tab_model_->AddObserver(this);
}
~ObserverHelper() override { tab_model_->RemoveObserver(this); }
// TabModelObserver:
void OnTabGroupCreated(tab_groups::TabGroupId group_id) override {
owner_->OnTabGroupCreated(group_id);
}
private:
raw_ptr<TabGroupsMoveFunction> owner_;
raw_ptr<TabModel> tab_model_;
};
#endif // BUILDFLAG(IS_ANDROID)
TabGroupsMoveFunction::TabGroupsMoveFunction() = default;
TabGroupsMoveFunction::~TabGroupsMoveFunction() = default;
ExtensionFunction::ResponseAction TabGroupsMoveFunction::Run() {
std::optional<api::tab_groups::Move::Params> params =
api::tab_groups::Move::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
int group_id = params->group_id;
int new_index = params->move_properties.index;
const auto& window_id = params->move_properties.window_id;
tab_groups::TabGroupId group = tab_groups::TabGroupId::CreateEmpty();
bool cross_window = false;
std::string error;
const bool group_moved =
MoveGroup(group_id, new_index, window_id, &group, &cross_window, &error);
if (!group_moved) {
return RespondNow(Error(std::move(error)));
}
#if BUILDFLAG(IS_ANDROID)
if (cross_window) {
// Cross window group moves are asynchronous on Android. OnTabGroupCreated()
// will be called later when the group is created in the new window.
return RespondLater();
}
#endif // BUILDFLAG(IS_ANDROID)
if (!has_callback()) {
return RespondNow(NoArguments());
}
auto group_object = ExtensionTabUtil::CreateTabGroupObject(group);
CHECK(group_object);
return RespondNow(
ArgumentList(api::tab_groups::Get::Results::Create(*group_object)));
}
bool TabGroupsMoveFunction::MoveGroup(int group_id,
int new_index,
const std::optional<int>& window_id,
tab_groups::TabGroupId* group,
bool* cross_window,
std::string* error) {
WindowController* source_window = nullptr;
tab_groups::TabGroupVisualData visual_data;
if (!ExtensionTabUtil::GetGroupById(
group_id, browser_context(), include_incognito_information(),
&source_window, group, &visual_data, error)) {
return false;
}
if (!ExtensionTabUtil::IsTabStripEditable(*source_window->profile())) {
*error = ExtensionTabUtil::kTabStripNotEditableError;
return false;
}
auto* source_browser = source_window->GetBrowserWindowInterface();
if (!source_browser) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
if (!ExtensionTabUtil::SupportsTabGroups(source_browser)) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
TabListInterface* source_tab_list = TabListInterface::From(source_browser);
if (!source_tab_list) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
gfx::Range tabs = source_tab_list->GetTabGroupTabIndices(*group);
if (tabs.length() == 0) {
return false;
}
if (window_id) {
WindowController* target_window = nullptr;
if (!windows_util::GetControllerFromWindowID(
this, *window_id, WindowController::GetAllWindowFilter(),
&target_window, error)) {
return false;
}
BrowserWindowInterface* target_browser =
target_window->GetBrowserWindowInterface();
if (!target_browser) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
// TODO(crbug.com/40638654): Rather than calling is_type_normal(), should
// this call SupportsWindowFeature(Browser::kFeatureTabstrip)?
if (target_browser->GetType() != BrowserWindowInterface::TYPE_NORMAL) {
*error = ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError;
return false;
}
if (target_window->profile() != source_window->profile()) {
*error = ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError;
return false;
}
// If windowId is different from the current window, move between windows.
if (target_browser != source_browser) {
*cross_window = true;
#if BUILDFLAG(IS_ANDROID)
// Observe for OnTabGroupCreated() notifications, because cross-window
// moves are asynchronous on Android.
TabModel* target_tab_model =
TabModelList::FindTabModelWithWindowSessionId(
target_browser->GetSessionID());
if (!target_tab_model) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
observer_helper_ =
std::make_unique<ObserverHelper>(this, target_tab_model);
#endif // BUILDFLAG(IS_ANDROID)
return MoveTabGroupBetweenBrowsers(source_browser, target_browser, *group,
visual_data, tabs, new_index, error);
}
}
// Perform a move within the same window.
// When moving to the right, adjust the target index for the size of the
// group, since the group itself may occupy several indices to the right.
const int start_index = tabs.start();
const int new_index_before_group_is_removed =
new_index > start_index ? new_index + tabs.length() : new_index;
if (!IndexSupportsGroupMove(source_tab_list,
new_index_before_group_is_removed, error)) {
return false;
}
// Unlike when moving between windows, the index should be clamped to
// count() - (#num of tabs in group being moved). Since the current tab(s)
// being moved are within the same tabstrip, they can't be added beyond the
// end of the occupied indices, but rather just shifted among them.
const int size_after_group_removed =
source_tab_list->GetTabCount() - tabs.length();
if (new_index >= size_after_group_removed || new_index < 0) {
new_index = size_after_group_removed;
}
if (new_index == start_index) {
return true;
}
source_tab_list->MoveGroupTo(*group, new_index);
return true;
}
bool TabGroupsMoveFunction::MoveTabGroupBetweenBrowsers(
BrowserWindowInterface* source_browser,
BrowserWindowInterface* target_browser,
const tab_groups::TabGroupId& group,
const tab_groups::TabGroupVisualData& visual_data,
const gfx::Range& tabs,
int new_index,
std::string* error) {
TabListInterface* target_tab_list =
ExtensionTabUtil::GetEditableTabList(*target_browser);
if (!target_tab_list) {
*error = ExtensionTabUtil::kTabStripNotEditableError;
return false;
}
if (!ExtensionTabUtil::SupportsTabGroups(target_browser)) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
if (new_index > target_tab_list->GetTabCount() || new_index < 0) {
new_index = target_tab_list->GetTabCount();
}
if (!IndexSupportsGroupMove(target_tab_list, new_index, error)) {
return false;
}
TabListInterface* source_tab_list = TabListInterface::From(source_browser);
if (!source_tab_list) {
*error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
return false;
}
// Pausing Saved Tab Groups is handled in TabListBridge on Win/Mac/Linux and
// in MultiInstanceManagerApi31 on Android.
source_tab_list->MoveTabGroupToWindow(group, target_browser->GetSessionID(),
new_index);
return true;
}
#if BUILDFLAG(IS_ANDROID)
void TabGroupsMoveFunction::OnTabGroupCreated(tab_groups::TabGroupId group_id) {
observer_helper_.reset();
auto group_object = ExtensionTabUtil::CreateTabGroupObject(group_id);
CHECK(group_object);
Respond(ArgumentList(api::tab_groups::Get::Results::Create(*group_object)));
}
#endif // BUILDFLAG(IS_ANDROID)
} // namespace extensions