Commit 2930e73f authored by Connie Wan's avatar Connie Wan Committed by Chromium LUCI CQ

Add events to Tab Groups extension API

Bug: 1106846
Change-Id: Icb8f2ae377e68eef47636bd881947ad0b3e8281d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2419755
Commit-Queue: Connie Wan <connily@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#843374}
parent 3f958afb
......@@ -378,6 +378,10 @@ static_library("extensions") {
"api/tab_groups/tab_groups_api.h",
"api/tab_groups/tab_groups_constants.cc",
"api/tab_groups/tab_groups_constants.h",
"api/tab_groups/tab_groups_event_router.cc",
"api/tab_groups/tab_groups_event_router.h",
"api/tab_groups/tab_groups_event_router_factory.cc",
"api/tab_groups/tab_groups_event_router_factory.h",
"api/tab_groups/tab_groups_util.cc",
"api/tab_groups/tab_groups_util.h",
"api/tabs/app_base_window.cc",
......
......@@ -5,7 +5,12 @@
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/extensions/api/tab_groups.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/test_event_router_observer.h"
namespace extensions {
......@@ -26,5 +31,37 @@ IN_PROC_BROWSER_TEST_F(TabGroupsApiTest, TestTabGroupsWorks) {
<< message_;
}
// Tests that events are restricted to their respective browser contexts,
// especially between on-the-record and off-the-record browsers.
// Note: unit tests don't support multiple profiles, so this has to be a browser
// test.
IN_PROC_BROWSER_TEST_F(TabGroupsApiTest, TestTabGroupEventsAcrossProfiles) {
Browser* incognito_browser =
OpenURLOffTheRecord(browser()->profile(), GURL("about:blank"));
// The EventRouter is shared between on- and off-the-record profiles, so
// this observer will catch events for each. To verify that the events are
// restricted to their respective contexts, we check the event metadata.
TestEventRouterObserver event_observer(
EventRouter::Get(browser()->profile()));
browser()->tab_strip_model()->AddToNewGroup({0});
ASSERT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnCreated::kEventName));
Event* normal_event =
event_observer.events().at(api::tab_groups::OnCreated::kEventName).get();
EXPECT_EQ(normal_event->restrict_to_browser_context, browser()->profile());
event_observer.ClearEvents();
incognito_browser->tab_strip_model()->AddToNewGroup({0});
ASSERT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnCreated::kEventName));
Event* incognito_event =
event_observer.events().at(api::tab_groups::OnCreated::kEventName).get();
EXPECT_EQ(incognito_event->restrict_to_browser_context,
incognito_browser->profile());
}
} // namespace
} // namespace extensions
......@@ -14,6 +14,8 @@
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_constants.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_event_router.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_event_router_factory.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_util.h"
#include "chrome/browser/extensions/extension_function_test_utils.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
......@@ -25,12 +27,18 @@
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/test_browser_window.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/sessions/content/session_tab_helper.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 "content/public/browser/browser_context.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/test_event_router_observer.h"
#include "extensions/common/constants.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_builder.h"
......@@ -70,6 +78,17 @@ scoped_refptr<const Extension> CreateTabGroupsExtension() {
.Build();
}
std::unique_ptr<KeyedService> BuildTabGroupsEventRouter(
content::BrowserContext* context) {
return std::make_unique<TabGroupsEventRouter>(context);
}
std::unique_ptr<KeyedService> BuildEventRouter(
content::BrowserContext* context) {
return std::make_unique<extensions::EventRouter>(
context, ExtensionPrefs::Get(context));
}
} // namespace
class TabGroupsApiUnitTest : public ExtensionServiceTestBase {
......@@ -125,6 +144,16 @@ void TabGroupsApiUnitTest::SetUp() {
browser_->tab_strip_model()->AppendWebContents(std::move(contents),
/* foreground */ true);
}
TabGroupsEventRouterFactory::GetInstance()->SetTestingFactory(
browser_context(), base::BindRepeating(&BuildTabGroupsEventRouter));
EventRouterFactory::GetInstance()->SetTestingFactory(
browser_context(), base::BindRepeating(&BuildEventRouter));
// We need to call TabGroupsEventRouterFactory::Get() in order to instantiate
// the keyed service, since it's not created by default in unit tests.
TabGroupsEventRouterFactory::Get(browser_context());
}
void TabGroupsApiUnitTest::TearDown() {
......@@ -411,7 +440,7 @@ TEST_F(TabGroupsApiUnitTest, TabGroupsMoveAcrossWindows) {
EXPECT_EQ(group, tab_strip_model2->GetTabGroupForTab(3).value());
// Clean up.
browser2->tab_strip_model()->CloseAllTabs();
tab_strip_model2->CloseAllTabs();
}
// Test that a group is cannot be moved into the pinned tabs region.
......@@ -462,4 +491,58 @@ TEST_F(TabGroupsApiUnitTest, TabGroupsMoveToOtherGroupError) {
error);
}
TEST_F(TabGroupsApiUnitTest, TabGroupsOnCreated) {
TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));
browser()->tab_strip_model()->AddToNewGroup({1, 2, 3});
EXPECT_EQ(2u, event_observer.events().size());
EXPECT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnCreated::kEventName));
EXPECT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnUpdated::kEventName));
}
TEST_F(TabGroupsApiUnitTest, TabGroupsOnUpdated) {
TabStripModel* tab_strip_model = browser()->tab_strip_model();
tab_groups::TabGroupId group = tab_strip_model->AddToNewGroup({1, 2, 3});
TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));
tab_groups::TabGroupVisualData visual_data(base::ASCIIToUTF16("Title"),
tab_groups::TabGroupColorId::kRed);
tab_strip_model->group_model()->GetTabGroup(group)->SetVisualData(
visual_data);
EXPECT_EQ(1u, event_observer.events().size());
EXPECT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnUpdated::kEventName));
}
TEST_F(TabGroupsApiUnitTest, TabGroupsOnRemoved) {
TabStripModel* tab_strip_model = browser()->tab_strip_model();
tab_strip_model->AddToNewGroup({1, 2, 3});
TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));
tab_strip_model->RemoveFromGroup({1, 2, 3});
EXPECT_EQ(1u, event_observer.events().size());
EXPECT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnRemoved::kEventName));
}
TEST_F(TabGroupsApiUnitTest, TabGroupsOnMoved) {
TabStripModel* tab_strip_model = browser()->tab_strip_model();
tab_groups::TabGroupId group = tab_strip_model->AddToNewGroup({1, 2, 3});
TestEventRouterObserver event_observer(EventRouter::Get(browser_context()));
tab_strip_model->MoveGroupTo(group, 0);
EXPECT_EQ(1u, event_observer.events().size());
EXPECT_TRUE(base::Contains(event_observer.events(),
api::tab_groups::OnMoved::kEventName));
}
} // namespace extensions
......@@ -7,10 +7,6 @@
namespace extensions {
namespace tab_groups_constants {
const char kCollapsedKey[] = "collapsed";
const char kColorKey[] = "color";
const char kTitleKey[] = "title";
const char kCannotMoveGroupIntoMiddleOfOtherGroupError[] =
"Cannot move the group to an index that is in the middle of another group.";
const char kCannotMoveGroupIntoMiddleOfPinnedTabsError[] =
......
......@@ -10,11 +10,6 @@ namespace extensions {
// Constants used for the Tab Groups API.
namespace tab_groups_constants {
// Keys used in serializing group data & events.
extern const char kCollapsedKey[];
extern const char kColorKey[];
extern const char kTitleKey[];
// Error messages.
extern const char kCannotMoveGroupIntoMiddleOfOtherGroupError[];
extern const char kCannotMoveGroupIntoMiddleOfPinnedTabsError[];
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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_event_router.h"
#include <utility>
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_util.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/tabs/tab_group.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.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"
namespace extensions {
TabGroupsEventRouter::TabGroupsEventRouter(content::BrowserContext* context)
: profile_(Profile::FromBrowserContext(context)),
event_router_(EventRouter::Get(context)),
browser_tab_strip_tracker_(this, this) {
browser_tab_strip_tracker_.Init();
}
void TabGroupsEventRouter::OnTabGroupChanged(const TabGroupChange& change) {
switch (change.type) {
case TabGroupChange::kCreated: {
DispatchGroupCreated(change.group);
break;
}
case TabGroupChange::kClosed: {
DispatchGroupRemoved(change.group);
break;
}
case TabGroupChange::kMoved: {
DispatchGroupMoved(change.group);
break;
}
case TabGroupChange::kVisualsChanged: {
DispatchGroupUpdated(change.group);
break;
}
case TabGroupChange::kContentsChanged:
case TabGroupChange::kEditorOpened:
break;
}
return;
}
bool TabGroupsEventRouter::ShouldTrackBrowser(Browser* browser) {
return profile_ == browser->profile() &&
ExtensionTabUtil::BrowserSupportsTabs(browser);
}
void TabGroupsEventRouter::DispatchGroupCreated(tab_groups::TabGroupId group) {
std::unique_ptr<base::ListValue> args(api::tab_groups::OnCreated::Create(
*tab_groups_util::CreateTabGroupObject(group)));
DispatchEvent(events::TAB_GROUPS_ON_CREATED,
api::tab_groups::OnCreated::kEventName, std::move(args));
}
void TabGroupsEventRouter::DispatchGroupRemoved(tab_groups::TabGroupId group) {
std::unique_ptr<base::ListValue> args(api::tab_groups::OnRemoved::Create(
*tab_groups_util::CreateTabGroupObject(group)));
DispatchEvent(events::TAB_GROUPS_ON_REMOVED,
api::tab_groups::OnRemoved::kEventName, std::move(args));
}
void TabGroupsEventRouter::DispatchGroupMoved(tab_groups::TabGroupId group) {
std::unique_ptr<base::ListValue> args(api::tab_groups::OnMoved::Create(
*tab_groups_util::CreateTabGroupObject(group)));
DispatchEvent(events::TAB_GROUPS_ON_MOVED,
api::tab_groups::OnMoved::kEventName, std::move(args));
}
void TabGroupsEventRouter::DispatchGroupUpdated(tab_groups::TabGroupId group) {
std::unique_ptr<base::ListValue> args(api::tab_groups::OnUpdated::Create(
*tab_groups_util::CreateTabGroupObject(group)));
DispatchEvent(events::TAB_GROUPS_ON_UPDATED,
api::tab_groups::OnUpdated::kEventName, std::move(args));
}
void TabGroupsEventRouter::DispatchEvent(
events::HistogramValue histogram_value,
const std::string& event_name,
std::unique_ptr<base::ListValue> args) {
// |event_router_| can be null in tests.
if (!event_router_)
return;
auto event = std::make_unique<Event>(histogram_value, event_name,
std::move(args), profile_);
event_router_->BroadcastEvent(std::move(event));
}
} // namespace extensions
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_EXTENSIONS_API_TAB_GROUPS_TAB_GROUPS_EVENT_ROUTER_H_
#define CHROME_BROWSER_EXTENSIONS_API_TAB_GROUPS_TAB_GROUPS_EVENT_ROUTER_H_
#include <memory>
#include <string>
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "chrome/browser/ui/browser_tab_strip_tracker.h"
#include "chrome/browser/ui/browser_tab_strip_tracker_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "components/tab_groups/tab_group_id.h"
#include "extensions/browser/event_router.h"
namespace extensions {
// The TabGroupsEventRouter listens to group events and routes them to listeners
// inside extension process renderers.
// TabGroupsEventRouter will only route events from windows/tabs within a
// profile to extension processes in the same profile.
class TabGroupsEventRouter : public TabStripModelObserver,
public BrowserTabStripTrackerDelegate,
public KeyedService {
public:
explicit TabGroupsEventRouter(content::BrowserContext* context);
TabGroupsEventRouter(const TabGroupsEventRouter&) = delete;
TabGroupsEventRouter& operator=(const TabGroupsEventRouter&) = delete;
~TabGroupsEventRouter() override = default;
// TabStripModelObserver:
void OnTabGroupChanged(const TabGroupChange& change) override;
// BrowserTabStripTrackerDelegate:
bool ShouldTrackBrowser(Browser* browser) override;
private:
// Methods called from OnTabGroupChanged.
void DispatchGroupCreated(tab_groups::TabGroupId group);
void DispatchGroupRemoved(tab_groups::TabGroupId group);
void DispatchGroupMoved(tab_groups::TabGroupId group);
void DispatchGroupUpdated(tab_groups::TabGroupId group);
void DispatchEvent(events::HistogramValue histogram_value,
const std::string& event_name,
std::unique_ptr<base::ListValue> args);
Profile* const profile_;
EventRouter* const event_router_ = nullptr;
BrowserTabStripTracker browser_tab_strip_tracker_;
};
} // namespace extensions
#endif // CHROME_BROWSER_EXTENSIONS_API_TAB_GROUPS_TAB_GROUPS_EVENT_ROUTER_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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_event_router_factory.h"
#include "base/no_destructor.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_event_router.h"
#include "chrome/browser/profiles/incognito_helpers.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/event_router_factory.h"
namespace extensions {
// static
TabGroupsEventRouter* TabGroupsEventRouterFactory::Get(
content::BrowserContext* context) {
return static_cast<TabGroupsEventRouter*>(
GetInstance()->GetServiceForBrowserContext(context, /* create */ true));
}
// static
TabGroupsEventRouterFactory* TabGroupsEventRouterFactory::GetInstance() {
static base::NoDestructor<TabGroupsEventRouterFactory> g_factory;
return g_factory.get();
}
TabGroupsEventRouterFactory::TabGroupsEventRouterFactory()
: BrowserContextKeyedServiceFactory(
"TabGroupsEventRouter",
BrowserContextDependencyManager::GetInstance()) {
DependsOn(EventRouterFactory::GetInstance());
}
KeyedService* TabGroupsEventRouterFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
return new TabGroupsEventRouter(context);
}
content::BrowserContext* TabGroupsEventRouterFactory::GetBrowserContextToUse(
content::BrowserContext* context) const {
return chrome::GetBrowserContextOwnInstanceInIncognito(context);
}
bool TabGroupsEventRouterFactory::ServiceIsCreatedWithBrowserContext() const {
return true;
}
} // namespace extensions
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_EXTENSIONS_API_TAB_GROUPS_TAB_GROUPS_EVENT_ROUTER_FACTORY_H_
#define CHROME_BROWSER_EXTENSIONS_API_TAB_GROUPS_TAB_GROUPS_EVENT_ROUTER_FACTORY_H_
#include "base/no_destructor.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
namespace extensions {
class TabGroupsEventRouter;
// The factory responsible for creating the event router for the tabGroups API.
class TabGroupsEventRouterFactory : public BrowserContextKeyedServiceFactory {
public:
// Returns the TabGroupsEventRouter for |profile|, creating it if
// it is not yet created.
static TabGroupsEventRouter* Get(content::BrowserContext* context);
// Returns the TabGroupsEventRouterFactory instance.
static TabGroupsEventRouterFactory* GetInstance();
TabGroupsEventRouterFactory(const TabGroupsEventRouterFactory&) = delete;
TabGroupsEventRouterFactory& operator=(const TabGroupsEventRouterFactory&) =
delete;
protected:
friend base::NoDestructor<TabGroupsEventRouterFactory>;
private:
TabGroupsEventRouterFactory();
~TabGroupsEventRouterFactory() override = default;
// BrowserContextKeyedServiceFactory:
content::BrowserContext* GetBrowserContextToUse(
content::BrowserContext* context) const override;
bool ServiceIsCreatedWithBrowserContext() const override;
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* profile) const override;
};
} // namespace extensions
#endif // CHROME_BROWSER_EXTENSIONS_API_TAB_GROUPS_TAB_GROUPS_EVENT_ROUTER_FACTORY_H_
......@@ -33,6 +33,7 @@
#include "chrome/browser/extensions/api/signed_in_devices/signed_in_devices_manager.h"
#include "chrome/browser/extensions/api/system_indicator/system_indicator_manager_factory.h"
#include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_event_router_factory.h"
#include "chrome/browser/extensions/api/tabs/tabs_windows_api.h"
#include "chrome/browser/extensions/api/web_navigation/web_navigation_api.h"
#include "chrome/browser/extensions/api/webrtc_audio_private/webrtc_audio_private_api.h"
......@@ -126,6 +127,7 @@ void EnsureBrowserContextKeyedServiceFactoriesBuilt() {
extensions::SettingsOverridesAPI::GetFactoryInstance();
extensions::SignedInDevicesManager::GetFactoryInstance();
extensions::SystemIndicatorManagerFactory::GetInstance();
extensions::TabGroupsEventRouterFactory::GetInstance();
extensions::TabCaptureRegistry::GetFactoryInstance();
extensions::TabsWindowsAPI::GetFactoryInstance();
#if BUILDFLAG(IS_CHROMEOS_ASH)
......
......@@ -188,6 +188,56 @@
}
]
}
],
"events": [
{
"name": "onCreated",
"type": "function",
"description": "Fired when a group is created.",
"parameters": [
{
"$ref": "TabGroup",
"name": "group",
"description": "Details of the group that was created."
}
]
},
{
"name": "onUpdated",
"type": "function",
"description": "Fired when a group is updated.",
"parameters": [
{
"$ref": "TabGroup",
"name": "group",
"description": "Details of the group that was updated."
}
]
},
{
"name": "onMoved",
"type": "function",
"description": "Fired when a group is moved within a window. Move events are still fired for the individual tabs within the group, as well as for the group itself. This event is not fired when a group is moved between windows; instead, it will be removed from one window and created in another.",
"parameters": [
{
"$ref": "TabGroup",
"name": "group",
"description": "Details of the group that was moved."
}
]
},
{
"name": "onRemoved",
"type": "function",
"description": "Fired when a group is closed, either directly by the user or automatically because it contained zero tabs.",
"parameters": [
{
"$ref": "TabGroup",
"name": "group",
"description": "Details of the group that was removed."
}
]
}
]
}
]
......@@ -27,4 +27,18 @@ chrome.test.runTests([
});
});
},
function testCreateEventDispatched() {
let createdGroupId = -1;
chrome.tabGroups.onCreated.addListener((group) => {
chrome.test.assertEq(group.id, createdGroupId);
chrome.test.succeed();
});
chrome.tabs.create({}, (tab) => {
chrome.tabs.group({tabIds: tab.id}, (groupId) => {
createdGroupId = groupId;
});
});
}
]);
......@@ -490,6 +490,10 @@ enum HistogramValue {
FILE_MANAGER_PRIVATE_ON_TABLET_MODE_CHANGED = 468,
VIRTUAL_KEYBOARD_PRIVATE_ON_CLIPBOARD_HISTORY_CHANGED = 469,
VIRTUAL_KEYBOARD_PRIVATE_ON_CLIPBOARD_ITEM_UPDATED = 470,
TAB_GROUPS_ON_CREATED = 471,
TAB_GROUPS_ON_MOVED = 472,
TAB_GROUPS_ON_REMOVED = 473,
TAB_GROUPS_ON_UPDATED = 474,
// Last entry: Add new entries above, then run:
// python tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY
......
......@@ -95,3 +95,35 @@ chrome.tabGroups.update = function(groupId, updateProperties, callback) {};
* @see https://developer.chrome.com/extensions/tabGroups#method-move
*/
chrome.tabGroups.move = function(groupId, moveProperties, callback) {};
/**
* Fired when a group is created.
* @type {!ChromeEvent}
* @see https://developer.chrome.com/extensions/tabGroups#event-onCreated
*/
chrome.tabGroups.onCreated;
/**
* Fired when a group is updated.
* @type {!ChromeEvent}
* @see https://developer.chrome.com/extensions/tabGroups#event-onUpdated
*/
chrome.tabGroups.onUpdated;
/**
* Fired when a group is moved within a window. Move events are still fired for
* the individual tabs within the group, as well as for the group itself. This
* event is not fired when a group is moved between windows; instead, it will be
* removed from one window and created in another.
* @type {!ChromeEvent}
* @see https://developer.chrome.com/extensions/tabGroups#event-onMoved
*/
chrome.tabGroups.onMoved;
/**
* Fired when a group is closed, either directly by the user or automatically
* because it contained zero tabs.
* @type {!ChromeEvent}
* @see https://developer.chrome.com/extensions/tabGroups#event-onRemoved
*/
chrome.tabGroups.onRemoved;
......@@ -24381,6 +24381,10 @@ Called by update_extension_histograms.py.-->
<int value="469"
label="VIRTUAL_KEYBOARD_PRIVATE_ON_CLIPBOARD_HISTORY_CHANGED"/>
<int value="470" label="VIRTUAL_KEYBOARD_PRIVATE_ON_CLIPBOARD_ITEM_UPDATED"/>
<int value="471" label="TAB_GROUPS_ON_CREATED"/>
<int value="472" label="TAB_GROUPS_ON_MOVED"/>
<int value="473" label="TAB_GROUPS_ON_REMOVED"/>
<int value="474" label="TAB_GROUPS_ON_UPDATED"/>
</enum>
<enum name="ExtensionFileWriteResult">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment