// Copyright 2013 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.h" #include <string> #include <utility> #include "base/bind.h" #include "base/macros.h" #include "base/memory/weak_ptr.h" #include "base/scoped_observer.h" #include "base/strings/utf_string_conversions.h" #include "base/thread_task_runner_handle.h" #include "base/time/time.h" #include "chrome/browser/chrome_notification_types.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/generated_resources.h" #include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" #include "content/public/browser/notification_source.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/extension_registry_observer.h" #include "extensions/common/feature_switch.h" #include "ui/base/l10n/l10n_util.h" using extensions::Extension; namespace { // How long to wait for browser action animations to complete before retrying. const int kAnimationWaitMs = 50; // How often we retry when waiting for browser action animation to end. const int kAnimationWaitRetries = 10; // Class responsible for showing the bubble after it's installed. Owns itself. class ExtensionInstalledBubbleObserver : public content::NotificationObserver, public extensions::ExtensionRegistryObserver { public: explicit ExtensionInstalledBubbleObserver( scoped_ptr<ExtensionInstalledBubble> bubble) : bubble_(std::move(bubble)), extension_registry_observer_(this), animation_wait_retries_(0), weak_factory_(this) { // |extension| has been initialized but not loaded at this point. We need to // wait on showing the Bubble until the EXTENSION_LOADED gets fired. extension_registry_observer_.Add( extensions::ExtensionRegistry::Get(bubble_->browser()->profile())); registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING, content::Source<Browser>(bubble_->browser())); } void Run() { OnExtensionLoaded(nullptr, bubble_->extension()); } private: ~ExtensionInstalledBubbleObserver() override {} // content::NotificationObserver: void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) override { DCHECK_EQ(type, chrome::NOTIFICATION_BROWSER_CLOSING) << "Received unexpected notification"; // Browser is closing before the bubble was shown. // TODO(hcarmona): Look into logging this with the BubbleManager. delete this; } // extensions::ExtensionRegistryObserver: void OnExtensionLoaded(content::BrowserContext* browser_context, const extensions::Extension* extension) override { if (extension == bubble_->extension()) { // PostTask to ourself to allow all EXTENSION_LOADED Observers to run. // Only then can we be sure that a BrowserAction or PageAction has had // views created which we can inspect for the purpose of previewing of // pointing to them. base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&ExtensionInstalledBubbleObserver::Initialize, weak_factory_.GetWeakPtr())); } } void OnExtensionUnloaded( content::BrowserContext* browser_context, const extensions::Extension* extension, extensions::UnloadedExtensionInfo::Reason reason) override { if (extension == bubble_->extension()) { // Extension is going away. delete this; } } void Initialize() { DCHECK(bubble_); bubble_->Initialize(); Show(); } // Called internally via PostTask to show the bubble. void Show() { DCHECK(bubble_); // TODO(hcarmona): Investigate having the BubbleManager query the bubble // for |ShouldShow|. This is important because the BubbleManager may decide // to delay showing the bubble. if (bubble_->ShouldShow()) { // Must be 2 lines because the manager will take ownership of bubble. BubbleManager* manager = bubble_->browser()->GetBubbleManager(); manager->ShowBubble(std::move(bubble_)); delete this; return; } if (animation_wait_retries_++ < kAnimationWaitRetries) { base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, base::Bind(&ExtensionInstalledBubbleObserver::Show, weak_factory_.GetWeakPtr()), base::TimeDelta::FromMilliseconds(kAnimationWaitMs)); } else { // Retries are over; won't try again. // TODO(hcarmona): Look into logging this with the BubbleManager. delete this; } } // The bubble that will be shown when the extension has finished installing. scoped_ptr<ExtensionInstalledBubble> bubble_; ScopedObserver<extensions::ExtensionRegistry, extensions::ExtensionRegistryObserver> extension_registry_observer_; content::NotificationRegistrar registrar_; // The number of times to retry showing the bubble if the bubble_->browser() // action toolbar is animating. int animation_wait_retries_; base::WeakPtrFactory<ExtensionInstalledBubbleObserver> weak_factory_; DISALLOW_COPY_AND_ASSIGN(ExtensionInstalledBubbleObserver); }; // Returns the keybinding for an extension command, or a null if none exists. scoped_ptr<extensions::Command> GetCommand( const std::string& extension_id, Profile* profile, ExtensionInstalledBubble::BubbleType type) { scoped_ptr<extensions::Command> result; extensions::Command command; extensions::CommandService* command_service = extensions::CommandService::Get(profile); bool has_command = false; if (type == ExtensionInstalledBubble::BROWSER_ACTION) { has_command = command_service->GetBrowserActionCommand( extension_id, extensions::CommandService::ACTIVE, &command, nullptr); } else if (type == ExtensionInstalledBubble::PAGE_ACTION) { has_command = command_service->GetPageActionCommand( extension_id, extensions::CommandService::ACTIVE, &command, nullptr); } if (has_command) result.reset(new extensions::Command(command)); return result; } } // namespace // static void ExtensionInstalledBubble::ShowBubble( const extensions::Extension* extension, Browser* browser, const SkBitmap& icon) { // The ExtensionInstalledBubbleObserver will delete itself when the // ExtensionInstalledBubble is shown or when it can't be shown anymore. auto x = new ExtensionInstalledBubbleObserver( make_scoped_ptr(new ExtensionInstalledBubble(extension, browser, icon))); extensions::ExtensionRegistry* reg = extensions::ExtensionRegistry::Get(browser->profile()); if (reg->enabled_extensions().GetByID(extension->id())) { x->Run(); } } ExtensionInstalledBubble::ExtensionInstalledBubble(const Extension* extension, Browser* browser, const SkBitmap& icon) : extension_(extension), browser_(browser), icon_(icon), type_(GENERIC), options_(NONE), anchor_position_(ANCHOR_APP_MENU) { } ExtensionInstalledBubble::~ExtensionInstalledBubble() {} bool ExtensionInstalledBubble::ShouldClose(BubbleCloseReason reason) const { // Installing an extension triggers a navigation event that should be ignored. return reason != BUBBLE_CLOSE_NAVIGATED; } std::string ExtensionInstalledBubble::GetName() const { return "ExtensionInstalled"; } const content::RenderFrameHost* ExtensionInstalledBubble::OwningFrame() const { return nullptr; } base::string16 ExtensionInstalledBubble::GetHowToUseDescription() const { int message_id = 0; base::string16 extra; if (action_command_) extra = action_command_->accelerator().GetShortcutText(); switch (type_) { case BROWSER_ACTION: message_id = extra.empty() ? IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO : IDS_EXTENSION_INSTALLED_BROWSER_ACTION_INFO_WITH_SHORTCUT; break; case PAGE_ACTION: message_id = extra.empty() ? IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO : IDS_EXTENSION_INSTALLED_PAGE_ACTION_INFO_WITH_SHORTCUT; break; case OMNIBOX_KEYWORD: extra = base::UTF8ToUTF16(extensions::OmniboxInfo::GetKeyword(extension_)); message_id = IDS_EXTENSION_INSTALLED_OMNIBOX_KEYWORD_INFO; break; case GENERIC: break; } if (message_id == 0) return base::string16(); return extra.empty() ? l10n_util::GetStringUTF16(message_id) : l10n_util::GetStringFUTF16(message_id, extra); } void ExtensionInstalledBubble::Initialize() { bool extension_action_redesign_on = extensions::FeatureSwitch::extension_action_redesign()->IsEnabled(); if (extensions::ActionInfo::GetBrowserActionInfo(extension_)) { type_ = BROWSER_ACTION; } else if (extensions::ActionInfo::GetPageActionInfo(extension_) && (extensions::ActionInfo::IsVerboseInstallMessage(extension_) || extension_action_redesign_on)) { type_ = PAGE_ACTION; } else if (!extensions::OmniboxInfo::GetKeyword(extension_).empty()) { type_ = OMNIBOX_KEYWORD; } else { type_ = GENERIC; } action_command_ = GetCommand(extension_->id(), browser_->profile(), type_); if (extensions::sync_helper::IsSyncable(extension_) && SyncPromoUI::ShouldShowSyncPromo(browser_->profile())) options_ |= SIGN_IN_PROMO; // Determine the bubble options we want, based on the extension type. switch (type_) { case BROWSER_ACTION: case PAGE_ACTION: options_ |= HOW_TO_USE; if (has_command_keybinding()) { options_ |= SHOW_KEYBINDING; } else { // The How-To-Use text makes the bubble seem a little crowded when the // extension has a keybinding, so the How-To-Manage text is not shown // in those cases. options_ |= HOW_TO_MANAGE; } if (type_ == BROWSER_ACTION || extension_action_redesign_on) { // If the toolbar redesign is enabled, all bubbles for extensions point // to their toolbar action. anchor_position_ = ANCHOR_BROWSER_ACTION; } else { DCHECK_EQ(type_, PAGE_ACTION); anchor_position_ = ANCHOR_PAGE_ACTION; } break; case OMNIBOX_KEYWORD: options_ |= HOW_TO_USE | HOW_TO_MANAGE; anchor_position_ = ANCHOR_OMNIBOX; break; case GENERIC: anchor_position_ = ANCHOR_APP_MENU; break; } }