Home | History | Annotate | Download | only in extensions
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/extensions/external_install_ui.h"
      6 
      7 #include <string>
      8 
      9 #include "base/bind.h"
     10 #include "base/lazy_instance.h"
     11 #include "base/memory/ref_counted.h"
     12 #include "base/memory/scoped_ptr.h"
     13 #include "base/message_loop/message_loop.h"
     14 #include "base/metrics/histogram.h"
     15 #include "base/scoped_observer.h"
     16 #include "base/strings/utf_string_conversions.h"
     17 #include "chrome/app/chrome_command_ids.h"
     18 #include "chrome/browser/chrome_notification_types.h"
     19 #include "chrome/browser/extensions/extension_install_prompt.h"
     20 #include "chrome/browser/extensions/extension_install_ui.h"
     21 #include "chrome/browser/extensions/extension_service.h"
     22 #include "chrome/browser/extensions/extension_uninstall_dialog.h"
     23 #include "chrome/browser/extensions/webstore_data_fetcher.h"
     24 #include "chrome/browser/extensions/webstore_data_fetcher_delegate.h"
     25 #include "chrome/browser/profiles/profile.h"
     26 #include "chrome/browser/ui/browser.h"
     27 #include "chrome/browser/ui/browser_finder.h"
     28 #include "chrome/browser/ui/global_error/global_error.h"
     29 #include "chrome/browser/ui/global_error/global_error_service.h"
     30 #include "chrome/browser/ui/global_error/global_error_service_factory.h"
     31 #include "chrome/common/extensions/manifest_url_handler.h"
     32 #include "content/public/browser/notification_details.h"
     33 #include "content/public/browser/notification_observer.h"
     34 #include "content/public/browser/notification_registrar.h"
     35 #include "content/public/browser/notification_source.h"
     36 #include "extensions/browser/extension_registry.h"
     37 #include "extensions/browser/extension_registry_observer.h"
     38 #include "extensions/common/constants.h"
     39 #include "grit/generated_resources.h"
     40 #include "ui/base/l10n/l10n_util.h"
     41 #include "ui/gfx/image/image.h"
     42 #include "ui/gfx/image/image_skia_operations.h"
     43 
     44 namespace extensions {
     45 
     46 namespace {
     47 
     48 // Whether the external extension can use the streamlined bubble install flow.
     49 bool UseBubbleInstall(const Extension* extension, bool is_new_profile) {
     50   return ManifestURL::UpdatesFromGallery(extension) && !is_new_profile;
     51 }
     52 
     53 }  // namespace
     54 
     55 static const int kMenuCommandId = IDC_EXTERNAL_EXTENSION_ALERT;
     56 
     57 class ExternalInstallGlobalError;
     58 
     59 namespace extensions {
     60 class ExtensionRegistry;
     61 }
     62 
     63 // This class is refcounted to stay alive while we try and pull webstore data.
     64 class ExternalInstallDialogDelegate
     65     : public ExtensionInstallPrompt::Delegate,
     66       public WebstoreDataFetcherDelegate,
     67       public content::NotificationObserver,
     68       public base::RefCountedThreadSafe<ExternalInstallDialogDelegate> {
     69  public:
     70   ExternalInstallDialogDelegate(Browser* browser,
     71                                 ExtensionService* service,
     72                                 const Extension* extension,
     73                                 bool use_global_error);
     74 
     75   Browser* browser() { return browser_; }
     76 
     77  private:
     78   friend class base::RefCountedThreadSafe<ExternalInstallDialogDelegate>;
     79   friend class ExternalInstallGlobalError;
     80 
     81   virtual ~ExternalInstallDialogDelegate();
     82 
     83   // ExtensionInstallPrompt::Delegate:
     84   virtual void InstallUIProceed() OVERRIDE;
     85   virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
     86 
     87   // WebstoreDataFetcherDelegate:
     88   virtual void OnWebstoreRequestFailure() OVERRIDE;
     89   virtual void OnWebstoreResponseParseSuccess(
     90       scoped_ptr<base::DictionaryValue> webstore_data) OVERRIDE;
     91   virtual void OnWebstoreResponseParseFailure(
     92       const std::string& error) OVERRIDE;
     93 
     94   // content::NotificationObserver:
     95   virtual void Observe(int type,
     96                        const content::NotificationSource& source,
     97                        const content::NotificationDetails& details) OVERRIDE;
     98 
     99   // Show the install dialog to the user.
    100   void ShowInstallUI();
    101 
    102   // The UI for showing the install dialog when enabling.
    103   scoped_ptr<ExtensionInstallPrompt> install_ui_;
    104   scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
    105 
    106   Browser* browser_;
    107   base::WeakPtr<ExtensionService> service_weak_;
    108   scoped_ptr<WebstoreDataFetcher> webstore_data_fetcher_;
    109   content::NotificationRegistrar registrar_;
    110   std::string extension_id_;
    111   bool use_global_error_;
    112 
    113   DISALLOW_COPY_AND_ASSIGN(ExternalInstallDialogDelegate);
    114 };
    115 
    116 // Only shows a menu item, no bubble. Clicking the menu item shows
    117 // an external install dialog.
    118 class ExternalInstallMenuAlert : public GlobalErrorWithStandardBubble,
    119                                  public content::NotificationObserver,
    120                                  public ExtensionRegistryObserver {
    121  public:
    122   ExternalInstallMenuAlert(ExtensionService* service,
    123                            const Extension* extension);
    124   virtual ~ExternalInstallMenuAlert();
    125 
    126   // GlobalError implementation.
    127   virtual Severity GetSeverity() OVERRIDE;
    128   virtual bool HasMenuItem() OVERRIDE;
    129   virtual int MenuItemCommandID() OVERRIDE;
    130   virtual base::string16 MenuItemLabel() OVERRIDE;
    131   virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
    132   virtual bool HasBubbleView() OVERRIDE;
    133   virtual base::string16 GetBubbleViewTitle() OVERRIDE;
    134   virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
    135   virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
    136   virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
    137   virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
    138   virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
    139   virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
    140 
    141  protected:
    142   ExtensionService* service_;
    143   const Extension* extension_;
    144 
    145  private:
    146   // Delete this instance after cleaning jobs.
    147   void Clean();
    148 
    149   // content::NotificationObserver implementation.
    150   virtual void Observe(int type,
    151                        const content::NotificationSource& source,
    152                        const content::NotificationDetails& details) OVERRIDE;
    153 
    154   // ExtensionRegistryObserver implementation.
    155   virtual void OnExtensionLoaded(content::BrowserContext* browser_context,
    156                                  const Extension* extension) OVERRIDE;
    157 
    158   content::NotificationRegistrar registrar_;
    159 
    160   // Listen to extension load notifications.
    161   ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver>
    162       extension_registry_observer_;
    163 
    164   DISALLOW_COPY_AND_ASSIGN(ExternalInstallMenuAlert);
    165 };
    166 
    167 // Shows a menu item and a global error bubble, replacing the install dialog.
    168 class ExternalInstallGlobalError : public ExternalInstallMenuAlert {
    169  public:
    170   ExternalInstallGlobalError(
    171       ExtensionService* service,
    172       const Extension* extension,
    173       ExternalInstallDialogDelegate* delegate,
    174       scoped_refptr<ExtensionInstallPrompt::Prompt> prompt);
    175   virtual ~ExternalInstallGlobalError();
    176 
    177   virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
    178   virtual bool HasBubbleView() OVERRIDE;
    179   virtual gfx::Image GetBubbleViewIcon() OVERRIDE;
    180   virtual base::string16 GetBubbleViewTitle() OVERRIDE;
    181   virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
    182   virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
    183   virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
    184   virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
    185   virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
    186   virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
    187 
    188  protected:
    189   // Ref-counted, but needs to be disposed of if we are dismissed without
    190   // having been clicked (perhaps because the user enabled the extension
    191   // manually).
    192   ExternalInstallDialogDelegate* delegate_;
    193   scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
    194 
    195  private:
    196   DISALLOW_COPY_AND_ASSIGN(ExternalInstallGlobalError);
    197 };
    198 
    199 static void CreateExternalInstallGlobalError(
    200     base::WeakPtr<ExtensionService> service,
    201     const std::string& extension_id,
    202     const ExtensionInstallPrompt::ShowParams& show_params,
    203     ExtensionInstallPrompt::Delegate* prompt_delegate,
    204     scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
    205   if (!service.get())
    206     return;
    207   const Extension* extension = service->GetInstalledExtension(extension_id);
    208   if (!extension)
    209     return;
    210   GlobalErrorService* error_service =
    211       GlobalErrorServiceFactory::GetForProfile(service->profile());
    212   if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
    213     return;
    214 
    215   ExternalInstallDialogDelegate* delegate =
    216       static_cast<ExternalInstallDialogDelegate*>(prompt_delegate);
    217   ExternalInstallGlobalError* error_bubble = new ExternalInstallGlobalError(
    218       service.get(), extension, delegate, prompt);
    219   error_service->AddGlobalError(error_bubble);
    220   // Show bubble immediately if possible.
    221   if (delegate->browser())
    222     error_bubble->ShowBubbleView(delegate->browser());
    223 }
    224 
    225 static void ShowExternalInstallDialog(
    226     ExtensionService* service,
    227     Browser* browser,
    228     const Extension* extension) {
    229   // This object manages its own lifetime.
    230   new ExternalInstallDialogDelegate(browser, service, extension, false);
    231 }
    232 
    233 // ExternalInstallDialogDelegate --------------------------------------------
    234 
    235 ExternalInstallDialogDelegate::ExternalInstallDialogDelegate(
    236     Browser* browser,
    237     ExtensionService* service,
    238     const Extension* extension,
    239     bool use_global_error)
    240     : browser_(browser),
    241       service_weak_(service->AsWeakPtr()),
    242       extension_id_(extension->id()),
    243       use_global_error_(use_global_error) {
    244   AddRef();  // Balanced in Proceed or Abort.
    245 
    246   prompt_ = new ExtensionInstallPrompt::Prompt(
    247       ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT);
    248 
    249   // If we don't have a browser, we can't go to the webstore to fetch data.
    250   // This should only happen in tests.
    251   if (!browser) {
    252     ShowInstallUI();
    253     return;
    254   }
    255 
    256   // Make sure to be notified if the owning profile is destroyed.
    257   registrar_.Add(this,
    258                  chrome::NOTIFICATION_PROFILE_DESTROYED,
    259                  content::Source<Profile>(browser->profile()));
    260 
    261   webstore_data_fetcher_.reset(new WebstoreDataFetcher(
    262       this,
    263       browser->profile()->GetRequestContext(),
    264       GURL::EmptyGURL(),
    265       extension->id()));
    266   webstore_data_fetcher_->Start();
    267 }
    268 
    269 void ExternalInstallDialogDelegate::OnWebstoreRequestFailure() {
    270   ShowInstallUI();
    271 }
    272 
    273 void ExternalInstallDialogDelegate::OnWebstoreResponseParseSuccess(
    274     scoped_ptr<base::DictionaryValue> webstore_data) {
    275   std::string localized_user_count;
    276   double average_rating;
    277   int rating_count;
    278   if (!webstore_data->GetString(kUsersKey, &localized_user_count) ||
    279       !webstore_data->GetDouble(kAverageRatingKey, &average_rating) ||
    280       !webstore_data->GetInteger(kRatingCountKey, &rating_count)) {
    281     // If we don't get a valid webstore response, short circuit, and continue
    282     // to show a prompt without webstore data.
    283     ShowInstallUI();
    284     return;
    285   }
    286 
    287   bool show_user_count = true;
    288   webstore_data->GetBoolean(kShowUserCountKey, &show_user_count);
    289 
    290   prompt_->SetWebstoreData(localized_user_count,
    291                            show_user_count,
    292                            average_rating,
    293                            rating_count);
    294 
    295   ShowInstallUI();
    296 }
    297 
    298 void ExternalInstallDialogDelegate::OnWebstoreResponseParseFailure(
    299     const std::string& error) {
    300   ShowInstallUI();
    301 }
    302 
    303 void ExternalInstallDialogDelegate::Observe(
    304     int type,
    305     const content::NotificationSource& source,
    306     const content::NotificationDetails& details) {
    307   DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
    308   // If the owning profile is destroyed, we need to abort so that we don't leak.
    309   InstallUIAbort(false);  // Not user initiated.
    310 }
    311 
    312 void ExternalInstallDialogDelegate::ShowInstallUI() {
    313   const Extension* extension = NULL;
    314   if (!service_weak_.get() ||
    315       !(extension = service_weak_->GetInstalledExtension(extension_id_))) {
    316     return;
    317   }
    318   install_ui_.reset(
    319       ExtensionInstallUI::CreateInstallPromptWithBrowser(browser_));
    320 
    321   const ExtensionInstallPrompt::ShowDialogCallback callback =
    322       use_global_error_ ?
    323           base::Bind(&CreateExternalInstallGlobalError,
    324                      service_weak_,
    325                      extension_id_) :
    326           ExtensionInstallPrompt::GetDefaultShowDialogCallback();
    327 
    328   install_ui_->ConfirmExternalInstall(this, extension, callback, prompt_);
    329 }
    330 
    331 ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() {
    332 }
    333 
    334 void ExternalInstallDialogDelegate::InstallUIProceed() {
    335   const Extension* extension = NULL;
    336   if (service_weak_.get() &&
    337       (extension = service_weak_->GetInstalledExtension(extension_id_))) {
    338     service_weak_->GrantPermissionsAndEnableExtension(extension);
    339   }
    340   Release();
    341 }
    342 
    343 void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated) {
    344   const Extension* extension = NULL;
    345 
    346   // Uninstall the extension if the abort was user initiated (and not, e.g., the
    347   // result of the window closing).
    348   // Otherwise, the extension will remain installed, but unacknowledged, so it
    349   // will be prompted again.
    350   if (user_initiated &&
    351       service_weak_.get() &&
    352       (extension = service_weak_->GetInstalledExtension(extension_id_))) {
    353     service_weak_->UninstallExtension(extension_id_, false, NULL);
    354   }
    355   Release();
    356 }
    357 
    358 // ExternalInstallMenuAlert -------------------------------------------------
    359 
    360 ExternalInstallMenuAlert::ExternalInstallMenuAlert(ExtensionService* service,
    361                                                    const Extension* extension)
    362     : service_(service),
    363       extension_(extension),
    364       extension_registry_observer_(this) {
    365   extension_registry_observer_.Add(ExtensionRegistry::Get(service->profile()));
    366   registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED,
    367                  content::Source<Profile>(service->profile()));
    368 }
    369 
    370 ExternalInstallMenuAlert::~ExternalInstallMenuAlert() {
    371 }
    372 
    373 GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() {
    374   return SEVERITY_LOW;
    375 }
    376 
    377 bool ExternalInstallMenuAlert::HasMenuItem() {
    378   return true;
    379 }
    380 
    381 int ExternalInstallMenuAlert::MenuItemCommandID() {
    382   return kMenuCommandId;
    383 }
    384 
    385 base::string16 ExternalInstallMenuAlert::MenuItemLabel() {
    386   int id = -1;
    387   if (extension_->is_app())
    388     id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP;
    389   else if (extension_->is_theme())
    390     id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME;
    391   else
    392     id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION;
    393   return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name()));
    394 }
    395 
    396 void ExternalInstallMenuAlert::ExecuteMenuItem(Browser* browser) {
    397   ShowExternalInstallDialog(service_, browser, extension_);
    398 }
    399 
    400 bool ExternalInstallMenuAlert::HasBubbleView() {
    401   return false;
    402 }
    403 base::string16 ExternalInstallMenuAlert::GetBubbleViewTitle() {
    404   return base::string16();
    405 }
    406 
    407 std::vector<base::string16> ExternalInstallMenuAlert::GetBubbleViewMessages() {
    408   return std::vector<base::string16>();
    409 }
    410 
    411 base::string16 ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() {
    412   return base::string16();
    413 }
    414 
    415 base::string16 ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() {
    416   return base::string16();
    417 }
    418 
    419 void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser* browser) {
    420   NOTREACHED();
    421 }
    422 
    423 void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed(
    424     Browser* browser) {
    425   NOTREACHED();
    426 }
    427 
    428 void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed(
    429     Browser* browser) {
    430   NOTREACHED();
    431 }
    432 
    433 void ExternalInstallMenuAlert::OnExtensionLoaded(
    434     content::BrowserContext* browser_context,
    435     const Extension* extension) {
    436   if (extension == extension_)
    437     Clean();
    438 }
    439 
    440 void ExternalInstallMenuAlert::Observe(
    441     int type,
    442     const content::NotificationSource& source,
    443     const content::NotificationDetails& details) {
    444   // The error is invalidated if the extension has been loaded or removed.
    445   DCHECK_EQ(type, chrome::NOTIFICATION_EXTENSION_REMOVED);
    446   const Extension* extension = content::Details<const Extension>(details).ptr();
    447   if (extension == extension_)
    448     Clean();
    449 }
    450 
    451 void ExternalInstallMenuAlert::Clean() {
    452   GlobalErrorService* error_service =
    453       GlobalErrorServiceFactory::GetForProfile(service_->profile());
    454   error_service->RemoveGlobalError(this);
    455   service_->AcknowledgeExternalExtension(extension_->id());
    456   delete this;
    457 }
    458 
    459 // ExternalInstallGlobalError -----------------------------------------------
    460 
    461 ExternalInstallGlobalError::ExternalInstallGlobalError(
    462     ExtensionService* service,
    463     const Extension* extension,
    464     ExternalInstallDialogDelegate* delegate,
    465     scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
    466     : ExternalInstallMenuAlert(service, extension),
    467       delegate_(delegate),
    468       prompt_(prompt) {
    469 }
    470 
    471 ExternalInstallGlobalError::~ExternalInstallGlobalError() {
    472   if (delegate_)
    473     delegate_->Release();
    474 }
    475 
    476 void ExternalInstallGlobalError::ExecuteMenuItem(Browser* browser) {
    477   ShowBubbleView(browser);
    478 }
    479 
    480 bool ExternalInstallGlobalError::HasBubbleView() {
    481   return true;
    482 }
    483 
    484 gfx::Image ExternalInstallGlobalError::GetBubbleViewIcon() {
    485   if (prompt_->icon().IsEmpty())
    486     return GlobalErrorWithStandardBubble::GetBubbleViewIcon();
    487   // Scale icon to a reasonable size.
    488   return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage(
    489       *prompt_->icon().ToImageSkia(),
    490       skia::ImageOperations::RESIZE_BEST,
    491       gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
    492                 extension_misc::EXTENSION_ICON_SMALL)));
    493 }
    494 
    495 base::string16 ExternalInstallGlobalError::GetBubbleViewTitle() {
    496   return prompt_->GetDialogTitle();
    497 }
    498 
    499 std::vector<base::string16>
    500 ExternalInstallGlobalError::GetBubbleViewMessages() {
    501   std::vector<base::string16> messages;
    502   messages.push_back(prompt_->GetHeading());
    503   if (prompt_->GetPermissionCount()) {
    504     messages.push_back(prompt_->GetPermissionsHeading());
    505     for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) {
    506       messages.push_back(l10n_util::GetStringFUTF16(
    507           IDS_EXTENSION_PERMISSION_LINE,
    508           prompt_->GetPermission(i)));
    509     }
    510   }
    511   // TODO(yoz): OAuth issue advice?
    512   return messages;
    513 }
    514 
    515 base::string16 ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() {
    516   return prompt_->GetAcceptButtonLabel();
    517 }
    518 
    519 base::string16 ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() {
    520   return prompt_->GetAbortButtonLabel();
    521 }
    522 
    523 void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser* browser) {
    524 }
    525 
    526 void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed(
    527     Browser* browser) {
    528   ExternalInstallDialogDelegate* delegate = delegate_;
    529   delegate_ = NULL;
    530   delegate->InstallUIProceed();
    531 }
    532 
    533 void ExternalInstallGlobalError::BubbleViewCancelButtonPressed(
    534     Browser* browser) {
    535   ExternalInstallDialogDelegate* delegate = delegate_;
    536   delegate_ = NULL;
    537   delegate->InstallUIAbort(true);
    538 }
    539 
    540 // Public interface ---------------------------------------------------------
    541 
    542 void AddExternalInstallError(ExtensionService* service,
    543                              const Extension* extension,
    544                              bool is_new_profile) {
    545   GlobalErrorService* error_service =
    546       GlobalErrorServiceFactory::GetForProfile(service->profile());
    547   if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
    548     return;
    549 
    550   if (UseBubbleInstall(extension, is_new_profile)) {
    551     Browser* browser = NULL;
    552 #if !defined(OS_ANDROID)
    553     browser = chrome::FindTabbedBrowser(service->profile(),
    554                                         true,
    555                                         chrome::GetActiveDesktop());
    556 #endif
    557     new ExternalInstallDialogDelegate(browser, service, extension, true);
    558   } else {
    559     error_service->AddGlobalError(
    560         new ExternalInstallMenuAlert(service, extension));
    561   }
    562 }
    563 
    564 void RemoveExternalInstallError(ExtensionService* service) {
    565   GlobalErrorService* error_service =
    566       GlobalErrorServiceFactory::GetForProfile(service->profile());
    567   GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
    568       kMenuCommandId);
    569   if (error) {
    570     error_service->RemoveGlobalError(error);
    571     delete error;
    572   }
    573 }
    574 
    575 bool HasExternalInstallError(ExtensionService* service) {
    576   GlobalErrorService* error_service =
    577       GlobalErrorServiceFactory::GetForProfile(service->profile());
    578   GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
    579       kMenuCommandId);
    580   return !!error;
    581 }
    582 
    583 bool HasExternalInstallBubble(ExtensionService* service) {
    584   GlobalErrorService* error_service =
    585       GlobalErrorServiceFactory::GetForProfile(service->profile());
    586   GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
    587       kMenuCommandId);
    588   return error && error->HasBubbleView();
    589 }
    590 
    591 }  // namespace extensions
    592