Commit b1eeea08 authored by Elly Fong-Jones's avatar Elly Fong-Jones Committed by Commit Bot

ExtensionInstalledBubble: exile Model

This change moves the ExtensionInstalledBubble's model, which was
previously a collection of anonymous functions in the Views implementation,
into //cbui/extensions as a separate class with unit tests. It also changes
the Views implementation to use this new class.

Change-Id: I3e379db88fcd4d40014c74f14c67b155dde3ac63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2007508
Commit-Queue: Elly Fong-Jones <ellyjones@chromium.org>
Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#739021}
parent 144c29c2
......@@ -3979,6 +3979,8 @@ jumbo_static_library("ui") {
"extensions/extension_install_ui_factory.h",
"extensions/extension_installed_bubble.cc",
"extensions/extension_installed_bubble.h",
"extensions/extension_installed_bubble_model.cc",
"extensions/extension_installed_bubble_model.h",
"extensions/extension_installed_waiter.cc",
"extensions/extension_installed_waiter.h",
"extensions/extension_message_bubble_bridge.cc",
......
// 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/ui/extensions/extension_installed_bubble_model.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/commands/command_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/sync/sync_promo_ui.h"
#include "chrome/common/extensions/api/extension_action/action_info.h"
#include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
#include "chrome/common/extensions/command.h"
#include "chrome/common/extensions/sync_helper.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "extensions/common/extension.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image_skia_operations.h"
namespace {
base::Optional<extensions::Command> CommandForExtensionAction(
const extensions::Extension* extension,
Profile* profile) {
const auto* info = extensions::ActionInfo::GetAnyActionInfo(extension);
if (!info)
return base::nullopt;
auto* service = extensions::CommandService::Get(profile);
extensions::Command command;
if (info->type == extensions::ActionInfo::TYPE_BROWSER &&
service->GetBrowserActionCommand(extension->id(),
extensions::CommandService::ACTIVE,
&command, nullptr)) {
return command;
}
if (info->type == extensions::ActionInfo::TYPE_PAGE &&
service->GetPageActionCommand(extension->id(),
extensions::CommandService::ACTIVE,
&command, nullptr)) {
return command;
}
return base::nullopt;
}
base::string16 MakeHowToUseText(const extensions::ActionInfo* action,
base::Optional<extensions::Command> command,
const std::string& keyword) {
base::string16 extra;
if (command.has_value())
extra = command->accelerator().GetShortcutText();
int message_id = 0;
if (action && action->type == extensions::ActionInfo::TYPE_BROWSER) {
message_id =
extra.empty()
? IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO
: IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT;
} else if (action && action->type == extensions::ActionInfo::TYPE_PAGE) {
message_id = extra.empty()
? IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO
: IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT;
} else if (!keyword.empty()) {
extra = base::UTF8ToUTF16(keyword);
message_id = IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO;
}
if (!message_id)
return base::string16();
return extra.empty() ? l10n_util::GetStringUTF16(message_id)
: l10n_util::GetStringFUTF16(message_id, extra);
}
} // namespace
ExtensionInstalledBubbleModel::ExtensionInstalledBubbleModel(
Profile* profile,
const extensions::Extension* extension,
const SkBitmap& icon)
: icon_(icon),
extension_id_(extension->id()),
extension_name_(extension->name()) {
const std::string& keyword = extensions::OmniboxInfo::GetKeyword(extension);
base::Optional<extensions::Command> command =
CommandForExtensionAction(extension, profile);
const auto* action_info = extensions::ActionInfo::GetAnyActionInfo(extension);
// TODO(ellyjones): There is no logical reason why TYPE_ACTION should be
// different here, but the existing bubble behaves this way.
const bool toolbar_action =
action_info && action_info->type != extensions::ActionInfo::TYPE_ACTION;
anchor_to_action_ = toolbar_action;
anchor_to_omnibox_ = !toolbar_action && !keyword.empty();
show_how_to_use_ =
(toolbar_action && !action_info->synthesized) || !keyword.empty();
// If there's a shortcut, don't show the how-to-manage text because it
// clutters the bubble.
show_how_to_manage_ = !command.has_value() || anchor_to_omnibox_;
show_key_binding_ = command.has_value();
show_sign_in_promo_ = extensions::sync_helper::IsSyncable(extension) &&
SyncPromoUI::ShouldShowSyncPromo(profile);
if (show_how_to_use_)
how_to_use_text_ = MakeHowToUseText(action_info, command, keyword);
}
ExtensionInstalledBubbleModel::~ExtensionInstalledBubbleModel() = default;
base::string16 ExtensionInstalledBubbleModel::GetHowToUseText() const {
DCHECK(show_how_to_use_);
return how_to_use_text_;
}
gfx::ImageSkia ExtensionInstalledBubbleModel::MakeIconOfSize(
const gfx::Size& wanted) const {
gfx::Size size(icon_.width(), icon_.height());
if (size.width() > wanted.width() || size.height() > wanted.height())
size.SetSize(wanted.width(), wanted.height());
return gfx::ImageSkiaOperations::CreateResizedImage(
gfx::ImageSkia::CreateFrom1xBitmap(icon_),
skia::ImageOperations::RESIZE_BEST, size);
}
// 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_UI_EXTENSIONS_EXTENSION_INSTALLED_BUBBLE_MODEL_H_
#define CHROME_BROWSER_UI_EXTENSIONS_EXTENSION_INSTALLED_BUBBLE_MODEL_H_
#include <string>
#include "base/strings/string16.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image_skia.h"
class Profile;
namespace extensions {
class Extension;
} // namespace extensions
// An ExtensionInstalledBubbleModel represents the state of an
// "extension installed" bubble. Instances of this class are entirely immutable
// after construction.
class ExtensionInstalledBubbleModel {
public:
ExtensionInstalledBubbleModel(Profile* profile,
const extensions::Extension* extension,
const SkBitmap& icon);
~ExtensionInstalledBubbleModel();
ExtensionInstalledBubbleModel(const ExtensionInstalledBubbleModel& other) =
delete;
ExtensionInstalledBubbleModel& operator=(
const ExtensionInstalledBubbleModel& other) = delete;
bool anchor_to_action() const { return anchor_to_action_; }
bool anchor_to_omnibox() const { return anchor_to_omnibox_; }
bool show_how_to_use() const { return show_how_to_use_; }
bool show_how_to_manage() const { return show_how_to_manage_; }
bool show_key_binding() const { return show_key_binding_; }
bool show_sign_in_promo() const { return show_sign_in_promo_; }
base::string16 GetHowToUseText() const;
gfx::ImageSkia MakeIconOfSize(const gfx::Size& size) const;
const std::string& extension_id() const { return extension_id_; }
const std::string& extension_name() const { return extension_name_; }
private:
// Whether the install bubble should anchor to the extension's action button
// or to the omnibox. At most one of these is true.
bool anchor_to_action_ = false;
bool anchor_to_omnibox_ = false;
// Whether to show the how-to-use and how-to-manage text in the install
// bubble.
bool show_how_to_use_ = false;
bool show_how_to_manage_ = false;
// Whether to show the extension's key binding in the install bubble.
bool show_key_binding_ = false;
// Whether to show a signin promo in the install bubble.
bool show_sign_in_promo_ = false;
base::string16 how_to_use_text_;
const SkBitmap icon_;
const std::string extension_id_;
const std::string extension_name_;
};
#endif // CHROME_BROWSER_UI_EXTENSIONS_EXTENSION_INSTALLED_BUBBLE_MODEL_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/ui/extensions/extension_installed_bubble_model.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/load_error_reporter.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/manifest_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/accelerators/accelerator.h"
using extensions::Extension;
class ExtensionInstalledBubbleModelTest : public BrowserWithTestWindowTest {
public:
ExtensionInstalledBubbleModelTest() = default;
~ExtensionInstalledBubbleModelTest() override = default;
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
extensions::LoadErrorReporter::Init(false);
extensions::TestExtensionSystem* extension_system =
static_cast<extensions::TestExtensionSystem*>(
extensions::ExtensionSystem::Get(profile()));
extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
extension_service_ =
extensions::ExtensionSystem::Get(profile())->extension_service();
}
void AddOmniboxKeyword(extensions::ExtensionBuilder* builder,
const std::string& keyword) {
auto info = std::make_unique<base::DictionaryValue>();
info->SetStringKey("keyword", keyword);
builder->SetManifestKey(extensions::manifest_keys::kOmnibox,
std::move(info));
}
void AddRegularAction(extensions::ExtensionBuilder* builder) {
builder->SetManifestKey(extensions::manifest_keys::kAction,
std::make_unique<base::DictionaryValue>());
}
void AddBrowserActionKeyBinding(extensions::ExtensionBuilder* builder,
const std::string& key) {
base::Value command(base::Value::Type::DICTIONARY);
command.SetStringKey("suggested_key", key);
command.SetStringKey("description", "Invoke the page action");
auto commands =
std::make_unique<base::Value>(base::Value::Type::DICTIONARY);
commands->SetKey(extensions::manifest_values::kBrowserActionCommandEvent,
std::move(command));
builder->SetManifestKey(extensions::manifest_keys::kCommands,
std::move(commands));
}
extensions::ExtensionService* extension_service() {
return extension_service_;
}
const SkBitmap empty_icon_;
private:
extensions::ExtensionService* extension_service_ = nullptr;
};
TEST_F(ExtensionInstalledBubbleModelTest, SyntheticPageActionExtension) {
// An extension with no action info in the manifest at all gets a synthesized
// page action.
auto extension = extensions::ExtensionBuilder("Foo").Build();
extension_service()->AddExtension(extension.get());
ExtensionInstalledBubbleModel model(browser()->profile(), extension.get(),
empty_icon_);
// It should anchor to the synthesized action...
EXPECT_TRUE(model.anchor_to_action());
EXPECT_FALSE(model.anchor_to_omnibox());
// ... but there should not be how-to-use text, since it has no actual way to
// activate it other than that synthesized action.
EXPECT_FALSE(model.show_how_to_use());
EXPECT_TRUE(model.show_how_to_manage());
EXPECT_FALSE(model.show_key_binding());
}
TEST_F(ExtensionInstalledBubbleModelTest, OmniboxExtension) {
// An extension with a regular action and an omnibox keyword...
auto builder = extensions::ExtensionBuilder("Foo");
AddOmniboxKeyword(&builder, "fookey");
AddRegularAction(&builder);
auto extension = builder.Build();
extension_service()->AddExtension(extension.get());
ExtensionInstalledBubbleModel model(browser()->profile(), extension.get(),
empty_icon_);
// ... should be anchored to the omnibox, not to the action ...
EXPECT_FALSE(model.anchor_to_action());
EXPECT_TRUE(model.anchor_to_omnibox());
// ... and should have how-to-use and how-to-manage text, but not show a key
// binding, since it doesn't have one.
EXPECT_TRUE(model.show_how_to_use());
EXPECT_TRUE(model.show_how_to_manage());
EXPECT_FALSE(model.show_key_binding());
}
TEST_F(ExtensionInstalledBubbleModelTest, PageActionExtension) {
// An extension with a page action...
auto extension =
extensions::ExtensionBuilder("Foo")
.SetAction(extensions::ExtensionBuilder::ActionType::PAGE_ACTION)
.Build();
extension_service()->AddExtension(extension.get());
ExtensionInstalledBubbleModel model(browser()->profile(), extension.get(),
empty_icon_);
// should anchor to that action
EXPECT_TRUE(model.anchor_to_action());
EXPECT_FALSE(model.anchor_to_omnibox());
// and have how-to-use and how-to-manage but no key binding, since it doesn't
// have one.
EXPECT_TRUE(model.show_how_to_use());
EXPECT_TRUE(model.show_how_to_manage());
EXPECT_FALSE(model.show_key_binding());
}
TEST_F(ExtensionInstalledBubbleModelTest, ExtensionWithKeyBinding) {
// An extension with a browser action and a key binding...
auto builder = extensions::ExtensionBuilder("Foo");
builder.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION);
AddBrowserActionKeyBinding(&builder, "Alt+Shift+E");
auto extension = builder.Build();
// Note that we have to OnExtensionInstalled() here rather than just adding it
// - hotkeys are picked up at install time, not add time.
extension_service()->OnExtensionInstalled(extension.get(),
syncer::StringOrdinal());
ExtensionInstalledBubbleModel model(browser()->profile(), extension.get(),
empty_icon_);
// Should have a how-to-use that lists the key, but *not* a how-to-manage,
// since it crowds the UI.
EXPECT_TRUE(model.show_how_to_use());
EXPECT_FALSE(model.show_how_to_manage());
EXPECT_TRUE(model.show_key_binding());
// Note that we can't just check for "Alt+Shift+E" in
// model.GetHowToUseText(), since on Mac, modifier keys are represented by
// special sigils rather than their textual names.
ui::Accelerator accelerator(ui::VKEY_E, ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN);
EXPECT_NE(base::string16::npos,
model.GetHowToUseText().find(accelerator.GetShortcutText()));
}
TEST_F(ExtensionInstalledBubbleModelTest, OmniboxKeywordAndSyntheticAction) {
auto builder = extensions::ExtensionBuilder("Foo");
AddOmniboxKeyword(&builder, "fookey");
auto extension = builder.Build();
extension_service()->AddExtension(extension.get());
ExtensionInstalledBubbleModel model(browser()->profile(), extension.get(),
empty_icon_);
// This extension has a synthesized action and an omnibox keyword. It should
// have how-to-use text, and be anchored to its (synthesized) page action.
EXPECT_TRUE(model.show_how_to_use());
EXPECT_TRUE(model.anchor_to_action());
}
// TODO(ellyjones): Add a test for a syncable extension with a sync-eligible
// profile, to test model.show_sign_in_promo(). Reference
// ExtensionServiceSyncTest for an example.
......@@ -4089,6 +4089,7 @@ test("unit_tests") {
"../browser/ui/content_settings/content_setting_media_image_model_unittest.mm",
"../browser/ui/exclusive_access/fullscreen_controller_state_unittest.cc",
"../browser/ui/extensions/extension_action_view_controller_unittest.cc",
"../browser/ui/extensions/extension_installed_bubble_model_unittest.cc",
"../browser/ui/extensions/extension_installed_waiter_unittest.cc",
"../browser/ui/extensions/extension_message_bubble_bridge_unittest.cc",
"../browser/ui/global_error/global_error_service_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