Commit 81f442ea authored by Christopher Cameron's avatar Christopher Cameron Committed by Commit Bot

MacPWAs: Add AppShimRegistry local storage

When launching a PWA, it should start where the user left off (that
is, with the profiles open that the user had open when they quit).
In order to do this, we need to save in local storage the list of
profiles for which each app was last active.

While we are creating local storage for PWAs, we often need to know
for what profiles an app is installed. This is currently implemented
by poking through the user-data-dir in the filesystem. Putting this
in local storage is much simpler.

To solve both of these problems, add the AppShimRegistry which
maintains the following structure in local storage.

  "app_shims":{
    "<app_id>":{
      "installed_profiles":[
        "<relative_profile_path_1>",
        "<relative_profile_path_2>",
      ],
      "last_active_profiles":[
        "<relative_profile_path_1>",
      ],
    }
  }

Add hooks to maintain these |installed_profiles| to
AppShortcutManager, because all shim creation and deletion
goes through that structure. We will at some point want to
move this to a PWA-specific component.

Do not yet add hooks to maintain |last_active_profiles| because this
patch is getting too big. Do add the code indicating where it will
be used in ExtensionAppShimHandler, and update its tests to not crash
when using AppShimRegistry.

Add lots o tests.

Bug: 1001213
Change-Id: I51d931567d9c3a4e1db143aa2b6164dcaa1a0f3e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1910719Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarAvi Drissman <avi@chromium.org>
Commit-Queue: ccameron <ccameron@chromium.org>
Cr-Commit-Position: refs/heads/master@{#715204}
parent c8bd2273
......@@ -27,6 +27,7 @@
#include "chrome/browser/apps/app_shim/app_shim_listener.h"
#include "chrome/browser/apps/app_shim/app_shim_termination_manager.h"
#include "chrome/browser/apps/launch_service/launch_service.h"
#include "chrome/browser/apps/platform_apps/app_shim_registry_mac.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/chrome_notification_types.h"
......@@ -623,11 +624,12 @@ void ExtensionAppShimHandler::OnAppEnabled(const base::FilePath& profile_path,
base::FilePath ExtensionAppShimHandler::SelectProfileForApp(
const std::string& app_id,
const base::FilePath& specified_profile_path,
const std::vector<base::FilePath>& profile_paths) const {
const std::vector<base::FilePath>& installed_profile_paths) const {
DCHECK(!installed_profile_paths.empty());
// If the specified profile path is valid, and the app is installed for that
// profile, then use the specified profile.
if (!specified_profile_path.empty()) {
if (base::Contains(profile_paths, specified_profile_path))
if (base::Contains(installed_profile_paths, specified_profile_path))
return specified_profile_path;
}
......@@ -643,9 +645,19 @@ base::FilePath ExtensionAppShimHandler::SelectProfileForApp(
}
}
// Otherwise, return the first profile. This assumes that |profile_paths|
// are sorted in most-recently-used order.
return profile_paths.front();
// See if there is a registered last-active profile.
{
std::set<base::FilePath> last_active_paths =
AppShimRegistry::Get()->GetLastActiveProfilesForApp(app_id);
if (!last_active_paths.empty()) {
// TODO(https://crbug.com/1001213): Allow opening multiple profiles at
// once.
return *last_active_paths.begin();
}
}
// Otherwise, return an arbitrary profile from |installed_profile_paths|.
return installed_profile_paths.front();
}
void ExtensionAppShimHandler::OnShimProcessConnectedAndProfilesRetrieved(
......
......@@ -19,11 +19,13 @@
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h"
#include "chrome/browser/apps/app_shim/app_shim_host_mac.h"
#include "chrome/browser/apps/platform_apps/app_shim_registry_mac.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/profiles/avatar_menu.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/mac/app_shim.mojom.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/browser/notification_service.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/common/extension.h"
......@@ -317,12 +319,18 @@ class TestHost : public AppShimHost {
class ExtensionAppShimHandlerTestBase : public testing::Test {
protected:
ExtensionAppShimHandlerTestBase() {}
~ExtensionAppShimHandlerTestBase() override {}
void SetUp() override {
local_state_ = std::make_unique<TestingPrefServiceSimple>();
AppShimRegistry::Get()->RegisterLocalPrefs(local_state_->registry());
AppShimRegistry::Get()->SetPrefServiceAndUserDataDirForTesting(
local_state_.get(), base::FilePath("/User/Data/Dir/"));
delegate_ = new MockDelegate;
handler_.reset(new TestingExtensionAppShimHandler(delegate_));
profile_path_a_ = base::FilePath("Profile A");
profile_path_b_ = base::FilePath("Profile B");
profile_path_a_ = base::FilePath("/User/Data/Dir/Profile A");
profile_path_b_ = base::FilePath("/User/Data/Dir/Profile B");
AppShimHostBootstrap::SetClient(handler_.get());
bootstrap_aa_ = (new TestingAppShimHostBootstrap(
profile_path_a_, kTestAppIdA,
......@@ -423,7 +431,7 @@ class ExtensionAppShimHandlerTestBase : public testing::Test {
.WillRepeatedly(Return());
}
~ExtensionAppShimHandlerTestBase() override {
void TearDown() override {
host_aa_unique_.reset();
host_ab_unique_.reset();
host_bb_unique_.reset();
......@@ -443,6 +451,9 @@ class ExtensionAppShimHandlerTestBase : public testing::Test {
delete bootstrap_aa_thethird_.get();
AppShimHostBootstrap::SetClient(nullptr);
AppShimRegistry::Get()->SetPrefServiceAndUserDataDirForTesting(
nullptr, base::FilePath());
}
void DoShimLaunch(base::WeakPtr<TestingAppShimHostBootstrap> bootstrap,
......@@ -531,6 +542,7 @@ class ExtensionAppShimHandlerTestBase : public testing::Test {
scoped_refptr<const Extension> extension_b_;
private:
std::unique_ptr<TestingPrefServiceSimple> local_state_;
DISALLOW_COPY_AND_ASSIGN(ExtensionAppShimHandlerTestBase);
};
......
......@@ -12,6 +12,8 @@ source_set("platform_apps") {
"app_load_service.h",
"app_load_service_factory.cc",
"app_load_service_factory.h",
"app_shim_registry_mac.cc",
"app_shim_registry_mac.h",
"app_termination_observer.cc",
"app_termination_observer.h",
"app_window_registry_util.cc",
......@@ -58,6 +60,7 @@ source_set("platform_apps") {
"//extensions/common",
"//extensions/common/api",
"//net",
"//services/preferences/public/cpp:cpp",
"//ui/gfx",
]
......
// Copyright 2019 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/apps/platform_apps/app_shim_registry_mac.h"
#include <memory>
#include <utility>
#include "base/logging.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "services/preferences/public/cpp/dictionary_value_update.h"
#include "services/preferences/public/cpp/scoped_pref_update.h"
namespace {
const char kAppShims[] = "app_shims";
const char kInstalledProfiles[] = "installed_profiles";
const char kLastActiveProfiles[] = "last_active_profiles";
} // namespace
// static
AppShimRegistry* AppShimRegistry::Get() {
static base::NoDestructor<AppShimRegistry> instance;
return instance.get();
}
void AppShimRegistry::RegisterLocalPrefs(PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kAppShims);
}
std::set<base::FilePath> AppShimRegistry::GetInstalledProfilesForApp(
const std::string& app_id) const {
std::set<base::FilePath> installed_profiles;
GetProfilesSetForApp(app_id, kInstalledProfiles, &installed_profiles);
return installed_profiles;
}
std::set<base::FilePath> AppShimRegistry::GetLastActiveProfilesForApp(
const std::string& app_id) const {
std::set<base::FilePath> last_active_profiles;
GetProfilesSetForApp(app_id, kLastActiveProfiles, &last_active_profiles);
// Cull out any profiles that are not installed.
std::set<base::FilePath> installed_profiles;
GetProfilesSetForApp(app_id, kInstalledProfiles, &installed_profiles);
for (auto it = last_active_profiles.begin();
it != last_active_profiles.end();) {
if (installed_profiles.count(*it))
it++;
else
last_active_profiles.erase(it++);
}
return last_active_profiles;
}
void AppShimRegistry::GetProfilesSetForApp(
const std::string& app_id,
const std::string& profiles_key,
std::set<base::FilePath>* profiles) const {
const base::DictionaryValue* cache =
GetPrefService()->GetDictionary(kAppShims);
const base::Value* app_info = cache->FindDictKey(app_id);
if (!app_info)
return;
const base::Value* profile_values = app_info->FindListKey(profiles_key);
if (!profile_values)
return;
for (const auto& profile_path_value : profile_values->GetList()) {
if (profile_path_value.is_string())
profiles->insert(GetFullProfilePath(profile_path_value.GetString()));
}
}
void AppShimRegistry::OnAppInstalledForProfile(const std::string& app_id,
const base::FilePath& profile) {
std::set<base::FilePath> installed_profiles =
GetInstalledProfilesForApp(app_id);
if (installed_profiles.count(profile))
return;
installed_profiles.insert(profile);
SetAppInfo(app_id, &installed_profiles, nullptr);
}
bool AppShimRegistry::OnAppUninstalledForProfile(
const std::string& app_id,
const base::FilePath& profile) {
auto installed_profiles = GetInstalledProfilesForApp(app_id);
auto found = installed_profiles.find(profile);
if (found != installed_profiles.end()) {
installed_profiles.erase(profile);
SetAppInfo(app_id, &installed_profiles, nullptr);
}
return installed_profiles.empty();
}
void AppShimRegistry::OnAppQuit(const std::string& app_id,
std::set<base::FilePath> last_active_profiles) {
SetAppInfo(app_id, nullptr, &last_active_profiles);
}
void AppShimRegistry::SetPrefServiceAndUserDataDirForTesting(
PrefService* pref_service,
const base::FilePath& user_data_dir) {
override_pref_service_ = pref_service;
override_user_data_dir_ = user_data_dir;
}
PrefService* AppShimRegistry::GetPrefService() const {
if (override_pref_service_)
return override_pref_service_;
return g_browser_process->local_state();
}
base::FilePath AppShimRegistry::GetFullProfilePath(
const std::string& profile_path) const {
base::FilePath relative_profile_path(profile_path);
if (!override_user_data_dir_.empty())
return override_user_data_dir_.Append(relative_profile_path);
ProfileManager* profile_manager = g_browser_process->profile_manager();
return profile_manager->user_data_dir().Append(relative_profile_path);
}
void AppShimRegistry::SetAppInfo(
const std::string& app_id,
const std::set<base::FilePath>* installed_profiles,
const std::set<base::FilePath>* last_active_profiles) {
prefs::ScopedDictionaryPrefUpdate update(GetPrefService(), kAppShims);
// If there are no installed profiles, clear the app's key.
if (installed_profiles && installed_profiles->empty()) {
update->Remove(app_id, nullptr);
return;
}
// Look up dictionary for the app.
std::unique_ptr<prefs::DictionaryValueUpdate> app_info;
if (!update->GetDictionaryWithoutPathExpansion(app_id, &app_info)) {
// If the key for the app doesn't exist, don't add it unless we are
// specifying a new |installed_profiles| (e.g, for when the app exits
// during uninstall and tells us its last-used profile after we just
// removed the entry for the app).
if (!installed_profiles)
return;
app_info = update->SetDictionaryWithoutPathExpansion(
app_id, std::make_unique<base::DictionaryValue>());
}
if (installed_profiles) {
auto values = std::make_unique<base::ListValue>();
for (const auto& profile : *installed_profiles)
values->AppendString(profile.BaseName().value());
app_info->Set(kInstalledProfiles, std::move(values));
}
if (last_active_profiles) {
auto values = std::make_unique<base::ListValue>();
for (const auto& profile : *last_active_profiles)
values->AppendString(profile.BaseName().value());
app_info->Set(kLastActiveProfiles, std::move(values));
}
}
// Copyright 2019 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_APPS_PLATFORM_APPS_APP_SHIM_REGISTRY_MAC_H_
#define CHROME_BROWSER_APPS_PLATFORM_APPS_APP_SHIM_REGISTRY_MAC_H_
#include <set>
#include <string>
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/no_destructor.h"
class PrefService;
class PrefRegistrySimple;
// This class is used to store information about which app shims have been
// installed for which profiles in local storage. This is used to:
// - Open the last active profile when an app shim is launched.
// - Populate the profile switcher menu in the app with only those profile
// for which the app is installed.
// - Only delete the app shim when it has been uninstalled for all profiles.
// All base::FilePath arguments to functions are expected to be full profile
// paths (e.g, the result of calling Profile::GetPath).
//
// This class should arguably be a extensions::ExtensionRegistryObserver. It
// is not (and instead is called by AppShortcutManager, which is one), because
// apps are in the process of being disentangled from extensions.
class AppShimRegistry {
public:
AppShimRegistry(const AppShimRegistry& other) = delete;
AppShimRegistry& operator=(const AppShimRegistry& other) = delete;
static AppShimRegistry* Get();
void RegisterLocalPrefs(PrefRegistrySimple* registry);
// Query the profiles paths for which the specified app is installed.
std::set<base::FilePath> GetInstalledProfilesForApp(
const std::string& app_id) const;
// Query the profiles paths that were last open in the app (which are the
// profiles to open when the app starts).
std::set<base::FilePath> GetLastActiveProfilesForApp(
const std::string& app_id) const;
// Called when an app is installed for a profile (or any other action that
// would create an app shim, e.g: launching the shim).
void OnAppInstalledForProfile(const std::string& app_id,
const base::FilePath& profile);
// Called when an app is uninstalled for a profile. Returns true if no
// profiles have this app installed anymore.
bool OnAppUninstalledForProfile(const std::string& app_id,
const base::FilePath& profile);
// Called when an app quits, providing a list of the profiles that were
// in use at the time of quitting.
void OnAppQuit(const std::string& app_id,
std::set<base::FilePath> active_profiles);
// Helper functions for testing.
void SetPrefServiceAndUserDataDirForTesting(
PrefService* pref_service,
const base::FilePath& user_data_dir);
protected:
friend class base::NoDestructor<AppShimRegistry>;
AppShimRegistry() = default;
~AppShimRegistry() = default;
PrefService* GetPrefService() const;
base::FilePath GetFullProfilePath(const std::string& profile_path) const;
// Helper function used by GetInstalledProfilesForApp and
// GetLastActiveProfilesForApp.
void GetProfilesSetForApp(const std::string& app_id,
const std::string& profiles_key,
std::set<base::FilePath>* profiles) const;
// Update the local storage for |app_id|. Update |installed_profiles| and
// |last_active_profiles| only if they are non-nullptr. If
// |installed_profiles| is non-nullptr and empty, remove the entry for
// |app_id|.
void SetAppInfo(const std::string& app_id,
const std::set<base::FilePath>* installed_profiles,
const std::set<base::FilePath>* last_active_profiles);
PrefService* override_pref_service_ = nullptr;
base::FilePath override_user_data_dir_;
};
#endif // CHROME_BROWSER_APPS_PLATFORM_APPS_APP_SHIM_REGISTRY_MAC_H_
// Copyright 2019 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/apps/platform_apps/app_shim_registry_mac.h"
#include "chrome/browser/apps/app_shim/extension_app_shim_handler_mac.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
class AppShimRegistryTest : public testing::Test {
public:
AppShimRegistryTest() = default;
~AppShimRegistryTest() override = default;
AppShimRegistryTest(const AppShimRegistryTest&) = delete;
AppShimRegistryTest& operator=(const AppShimRegistryTest&) = delete;
void SetUp() override {
local_state_ = std::make_unique<TestingPrefServiceSimple>();
registry_ = AppShimRegistry::Get();
registry_->RegisterLocalPrefs(local_state_->registry());
registry_->SetPrefServiceAndUserDataDirForTesting(local_state_.get(),
base::FilePath("/x/y/z"));
}
void TearDown() override {
registry_->SetPrefServiceAndUserDataDirForTesting(nullptr,
base::FilePath());
}
protected:
AppShimRegistry* registry_ = nullptr;
std::unique_ptr<TestingPrefServiceSimple> local_state_;
};
TEST_F(AppShimRegistryTest, Lifetime) {
const std::string app_id_a("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
const std::string app_id_b("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
base::FilePath profile_path_a("/x/y/z/Profile A");
base::FilePath profile_path_b("/x/y/z/Profile B");
base::FilePath profile_path_c("/x/y/z/Profile C");
std::set<base::FilePath> profiles;
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_a).size());
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_b).size());
// Ensure that OnAppUninstalledForProfile with no profiles installed is a
// no-op, and reports that the app is installed for no profiles.
EXPECT_TRUE(registry_->OnAppUninstalledForProfile(app_id_a, profile_path_a));
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_a).size());
// Ensure that OnAppQuit with no profiles installed is a no-op.
profiles.insert(profile_path_a);
registry_->OnAppQuit(app_id_a, profiles);
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_a).size());
EXPECT_EQ(0u, registry_->GetLastActiveProfilesForApp(app_id_a).size());
// Test installing for profile a.
registry_->OnAppInstalledForProfile(app_id_a, profile_path_a);
profiles = registry_->GetInstalledProfilesForApp(app_id_a);
EXPECT_EQ(profiles.size(), 1u);
EXPECT_TRUE(profiles.count(profile_path_a));
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_b).size());
// And installing for profile b.
registry_->OnAppInstalledForProfile(app_id_a, profile_path_b);
profiles = registry_->GetInstalledProfilesForApp(app_id_a);
EXPECT_EQ(profiles.size(), 2u);
EXPECT_TRUE(profiles.count(profile_path_a));
EXPECT_TRUE(profiles.count(profile_path_b));
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_b).size());
// Test OnAppQuit with a valid profile.
profiles.clear();
profiles.insert(profile_path_b);
registry_->OnAppQuit(app_id_a, profiles);
profiles = registry_->GetLastActiveProfilesForApp(app_id_a);
EXPECT_EQ(profiles.size(), 1u);
EXPECT_TRUE(profiles.count(profile_path_b));
// Test OnAppQuit with an invalid profile.
profiles.clear();
profiles.insert(profile_path_c);
registry_->OnAppQuit(app_id_a, profiles);
profiles = registry_->GetLastActiveProfilesForApp(app_id_a);
EXPECT_EQ(0u, registry_->GetLastActiveProfilesForApp(app_id_a).size());
// Test OnAppQuit with a valid and invalid profile. The invalid profile
// should be discarded.
profiles.clear();
profiles.insert(profile_path_a);
profiles.insert(profile_path_c);
registry_->OnAppQuit(app_id_a, profiles);
profiles = registry_->GetLastActiveProfilesForApp(app_id_a);
EXPECT_EQ(profiles.size(), 1u);
EXPECT_TRUE(profiles.count(profile_path_a));
// Uninstall for profile a. It should return false because it is still
// installed for profile b. The list of last active profiles should now
// be empty.
EXPECT_FALSE(registry_->OnAppUninstalledForProfile(app_id_a, profile_path_a));
EXPECT_EQ(0u, registry_->GetLastActiveProfilesForApp(app_id_a).size());
profiles = registry_->GetInstalledProfilesForApp(app_id_a);
EXPECT_EQ(profiles.size(), 1u);
EXPECT_TRUE(profiles.count(profile_path_b));
// Uninstall for profile b. It should return true because all profiles are
// gone.
EXPECT_TRUE(registry_->OnAppUninstalledForProfile(app_id_a, profile_path_b));
EXPECT_EQ(0u, registry_->GetInstalledProfilesForApp(app_id_a).size());
EXPECT_EQ(0u, registry_->GetLastActiveProfilesForApp(app_id_a).size());
}
} // namespace
......@@ -30,6 +30,7 @@
#include "extensions/common/extension_set.h"
#if defined(OS_MACOSX)
#include "chrome/browser/apps/platform_apps/app_shim_registry_mac.h"
#include "chrome/common/mac/app_mode_common.h"
#endif
......@@ -112,6 +113,11 @@ void AppShortcutManager::OnExtensionWillBeInstalled(
if (!extension->is_app())
return;
#if defined(OS_MACOSX)
AppShimRegistry::Get()->OnAppInstalledForProfile(extension->id(),
profile_->GetPath());
#endif
// If the app is being updated, update any existing shortcuts but do not
// create new ones. If it is being installed, automatically create a
// shortcut in the applications menu (e.g., Start Menu).
......@@ -127,6 +133,15 @@ void AppShortcutManager::OnExtensionUninstalled(
content::BrowserContext* browser_context,
const Extension* extension,
extensions::UninstallReason reason) {
#if defined(OS_MACOSX)
if (extension->is_app()) {
AppShimRegistry::Get()->OnAppUninstalledForProfile(extension->id(),
profile_->GetPath());
// TODO(https://crbug.com/1001213): Plumb the return result through
// DeleteAllShortcuts, to appropriately delete multi-profile apps.
}
#endif
web_app::DeleteAllShortcuts(profile_, extension);
}
......@@ -135,6 +150,7 @@ void AppShortcutManager::OnProfileWillBeRemoved(
if (profile_path != profile_->GetPath())
return;
// TODO(https://crbug.com/1001213): Update AppShimRegistry here.
web_app::internals::GetShortcutIOTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(&web_app::internals::DeleteAllShortcutsForProfile,
......
......@@ -327,6 +327,7 @@
#endif
#if defined(OS_MACOSX)
#include "chrome/browser/apps/platform_apps/app_shim_registry_mac.h"
#include "chrome/browser/ui/cocoa/apps/quit_with_apps_controller_mac.h"
#include "chrome/browser/ui/cocoa/confirm_quit.h"
#endif
......@@ -734,6 +735,7 @@ void RegisterLocalState(PrefRegistrySimple* registry) {
confirm_quit::RegisterLocalState(registry);
QuitWithAppsController::RegisterPrefs(registry);
system_media_permissions::RegisterSystemMediaPermissionStatesPrefs(registry);
AppShimRegistry::Get()->RegisterLocalPrefs(registry);
#endif
#if defined(OS_WIN) || defined(OS_MACOSX)
......
......@@ -4255,6 +4255,7 @@ test("unit_tests") {
"../../tools/json_schema_compiler/test/features_generation_unittest.cc",
"../browser/apps/app_shim/app_shim_host_mac_unittest.cc",
"../browser/apps/app_shim/extension_app_shim_handler_mac_unittest.cc",
"../browser/apps/platform_apps/app_shim_registry_mac_unittest.cc",
"../browser/autocomplete/keyword_extensions_delegate_impl_unittest.cc",
"../browser/browsing_data/counters/hosted_apps_counter_unittest.cc",
"../browser/extensions/active_tab_unittest.cc",
......
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