// Copyright (c) 2012 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.

#import "chrome/browser/ui/cocoa/extensions/extension_installed_bubble_controller.h"

#include <stddef.h>

#include "base/i18n/rtl.h"
#include "base/macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/bundle_installer.h"
#include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/chrome_style.h"
#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
#include "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/bubble_sync_promo_controller.h"
#include "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
#include "chrome/browser/ui/cocoa/extensions/bundle_util.h"
#include "chrome/browser/ui/cocoa/hover_close_button.h"
#include "chrome/browser/ui/cocoa/info_bubble_view.h"
#include "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
#include "chrome/browser/ui/cocoa/new_tab_button.h"
#include "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
#include "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
#include "chrome/browser/ui/extensions/extension_install_ui_factory.h"
#include "chrome/browser/ui/extensions/extension_installed_bubble.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/browser/ui/sync/sync_promo_ui.h"
#include "chrome/common/extensions/api/omnibox/omnibox_handler.h"
#include "chrome/common/extensions/sync_helper.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/bubble/bubble_controller.h"
#include "components/bubble/bubble_ui.h"
#include "components/signin/core/browser/signin_metrics.h"
#include "extensions/browser/install/extension_install_ui.h"
#include "extensions/common/extension.h"
#import "skia/ext/skia_utils_mac.h"
#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/cocoa/cocoa_base_utils.h"
#import "ui/base/cocoa/controls/hyperlink_text_view.h"
#include "ui/base/l10n/l10n_util.h"

using content::BrowserThread;
using extensions::BundleInstaller;
using extensions::Extension;

@interface ExtensionInstalledBubbleController ()

- (const Extension*)extension;
- (void)windowWillClose:(NSNotification*)notification;
- (void)windowDidResignKey:(NSNotification*)notification;
- (void)removePageActionPreviewIfNecessary;
- (NSPoint)calculateArrowPoint;
- (NSWindow*)initializeWindow;
- (int)calculateWindowHeight;
- (NSInteger)addExtensionList:(NSTextField*)headingMsg
                    itemsView:(NSView*)itemsView
                        state:(BundleInstaller::Item::State)state;
- (void)setMessageFrames:(int)newWindowHeight;
- (void)updateAnchorPosition;

@end  // ExtensionInstalledBubbleController ()

namespace {

class ExtensionInstalledBubbleBridge : public BubbleUi {
 public:
  explicit ExtensionInstalledBubbleBridge(
      ExtensionInstalledBubbleController* controller);
  ~ExtensionInstalledBubbleBridge() override;

 private:
  // BubbleUi:
  void Show(BubbleReference bubble_reference) override;
  void Close() override;
  void UpdateAnchorPosition() override;

  // Weak reference to the controller. |controller_| will outlive the bridge.
  ExtensionInstalledBubbleController* controller_;

  DISALLOW_COPY_AND_ASSIGN(ExtensionInstalledBubbleBridge);
};

ExtensionInstalledBubbleBridge::ExtensionInstalledBubbleBridge(
    ExtensionInstalledBubbleController* controller)
    : controller_(controller) {
}

ExtensionInstalledBubbleBridge::~ExtensionInstalledBubbleBridge() {
}

void ExtensionInstalledBubbleBridge::Show(BubbleReference bubble_reference) {
  [controller_ setBubbleReference:bubble_reference];
  [controller_ showWindow:controller_];
}

void ExtensionInstalledBubbleBridge::Close() {
  [controller_ doClose];
}

void ExtensionInstalledBubbleBridge::UpdateAnchorPosition() {
  [controller_ updateAnchorPosition];
}

}  // namespace

// Cocoa specific implementation.
bool ExtensionInstalledBubble::ShouldShow() {
  return true;
}

// Implemented here to create the platform specific instance of the BubbleUi.
scoped_ptr<BubbleUi> ExtensionInstalledBubble::BuildBubbleUi() {
  // |controller| is owned by the parent window.
  ExtensionInstalledBubbleController* controller =
      [[ExtensionInstalledBubbleController alloc]
          initWithParentWindow:browser()->window()->GetNativeWindow()
               extensionBubble:this];

  // The bridge to the C++ object that performs shared logic across platforms.
  // This tells the controller when to show the bubble.
  return make_scoped_ptr(new ExtensionInstalledBubbleBridge(controller));
}

@implementation ExtensionInstalledBubbleController

@synthesize bundle = bundle_;
@synthesize installedBubble = installedBubble_;
// Exposed for unit tests.
@synthesize heading = heading_;
@synthesize closeButton = closeButton_;
@synthesize howToUse = howToUse_;
@synthesize howToManage = howToManage_;
@synthesize appInstalledShortcutLink = appInstalledShortcutLink_;
@synthesize manageShortcutLink = manageShortcutLink_;
@synthesize promoContainer = promoContainer_;
@synthesize iconImage = iconImage_;
@synthesize pageActionPreviewShowing = pageActionPreviewShowing_;

- (id)initWithParentWindow:(NSWindow*)parentWindow
           extensionBubble:(ExtensionInstalledBubble*)extensionBubble {
  if ((self = [super initWithWindowNibPath:@"ExtensionInstalledBubble"
                              parentWindow:parentWindow
                                anchoredAt:NSZeroPoint])) {
    DCHECK(extensionBubble);
    const extensions::Extension* extension = extensionBubble->extension();
    browser_ = extensionBubble->browser();
    DCHECK(browser_);
    icon_.reset([skia::SkBitmapToNSImage(extensionBubble->icon()) retain]);
    pageActionPreviewShowing_ = NO;

    type_ = extension->is_app() ? extension_installed_bubble::kApp :
        extension_installed_bubble::kExtension;

    installedBubble_ = extensionBubble;
  }
  return self;
}

- (id)initWithParentWindow:(NSWindow*)parentWindow
                    bundle:(const BundleInstaller*)bundle
                   browser:(Browser*)browser {
  if ((self = [super initWithWindowNibPath:@"ExtensionInstalledBubbleBundle"
                              parentWindow:parentWindow
                                anchoredAt:NSZeroPoint])) {
    bundle_ = bundle;
    DCHECK(browser);
    browser_ = browser;
    icon_.reset([skia::SkBitmapToNSImage(SkBitmap()) retain]);
    pageActionPreviewShowing_ = NO;
    type_ = extension_installed_bubble::kBundle;
    [self showWindow:self];
  }
  return self;
}

- (const Extension*)extension {
  if (type_ == extension_installed_bubble::kBundle || !installedBubble_)
    return nullptr;
  return installedBubble_->extension();
}

- (void)windowWillClose:(NSNotification*)notification {
  // Turn off page action icon preview when the window closes, unless we
  // already removed it when the window resigned key status.
  [self removePageActionPreviewIfNecessary];
  browser_ = nullptr;
  [closeButton_ setTrackingEnabled:NO];
  [super windowWillClose:notification];
}

// The controller is the delegate of the window, so it receives "did resign
// key" notifications.  When key is resigned, close the window.
- (void)windowDidResignKey:(NSNotification*)notification {
  // If the browser window is closing, we need to remove the page action
  // immediately, otherwise the closing animation may overlap with
  // browser destruction.
  [self removePageActionPreviewIfNecessary];
  [super windowDidResignKey:notification];
}

- (IBAction)closeWindow:(id)sender {
  DCHECK([[self window] isVisible]);
  DCHECK([self bubbleReference]);
  bool didClose =
      [self bubbleReference]->CloseBubble(BUBBLE_CLOSE_USER_DISMISSED);
  DCHECK(didClose);
}

// Extracted to a function here so that it can be overridden for unit testing.
- (void)removePageActionPreviewIfNecessary {
  if (![self extension] || !pageActionPreviewShowing_)
    return;
  ExtensionAction* page_action =
      extensions::ExtensionActionManager::Get(browser_->profile())->
      GetPageAction(*[self extension]);
  if (!page_action)
    return;
  pageActionPreviewShowing_ = NO;

  BrowserWindowCocoa* window =
      static_cast<BrowserWindowCocoa*>(browser_->window());
  LocationBarViewMac* locationBarView =
      [window->cocoa_controller() locationBarBridge];
  locationBarView->SetPreviewEnabledPageAction(page_action,
                                               false);  // disables preview.
}

// The extension installed bubble points at the browser action icon or the
// page action icon (shown as a preview), depending on the extension type.
// We need to calculate the location of these icons and the size of the
// message itself (which varies with the title of the extension) in order
// to figure out the origin point for the extension installed bubble.
// TODO(mirandac): add framework to easily test extension UI components!
- (NSPoint)calculateArrowPoint {
  BrowserWindowCocoa* window =
      static_cast<BrowserWindowCocoa*>(browser_->window());
  NSPoint arrowPoint = NSZeroPoint;

  auto getAppMenuButtonAnchorPoint = [window]() {
    // Point at the bottom of the app menu menu.
    NSView* appMenuButton =
        [[window->cocoa_controller() toolbarController] appMenuButton];
    const NSRect bounds = [appMenuButton bounds];
    NSPoint anchor = NSMakePoint(NSMidX(bounds), NSMaxY(bounds));
    return [appMenuButton convertPoint:anchor toView:nil];
  };

  if (type_ == extension_installed_bubble::kApp) {
    TabStripView* view = [window->cocoa_controller() tabStripView];
    NewTabButton* button = [view getNewTabButton];
    NSRect bounds = [button bounds];
    NSPoint anchor = NSMakePoint(
        NSMidX(bounds),
        NSMaxY(bounds) - extension_installed_bubble::kAppsBubbleArrowOffset);
    arrowPoint = [button convertPoint:anchor toView:nil];
  } else if (type_ == extension_installed_bubble::kBundle) {
    arrowPoint = getAppMenuButtonAnchorPoint();
  } else {
    DCHECK(installedBubble_);
    switch (installedBubble_->anchor_position()) {
      case ExtensionInstalledBubble::ANCHOR_BROWSER_ACTION: {
        BrowserActionsController* controller =
            [[window->cocoa_controller() toolbarController]
                browserActionsController];
        arrowPoint = [controller popupPointForId:[self extension]->id()];
        break;
      }
      case ExtensionInstalledBubble::ANCHOR_PAGE_ACTION: {
        LocationBarViewMac* locationBarView =
            [window->cocoa_controller() locationBarBridge];

        ExtensionAction* page_action =
            extensions::ExtensionActionManager::Get(browser_->profile())->
            GetPageAction(*[self extension]);

        // Tell the location bar to show a preview of the page action icon,
        // which would ordinarily only be displayed on a page of the appropriate
        // type. We remove this preview when the extension installed bubble
        // closes.
        locationBarView->SetPreviewEnabledPageAction(page_action, true);
        pageActionPreviewShowing_ = YES;

        // Find the center of the bottom of the page action icon.
        arrowPoint = locationBarView->GetPageActionBubblePoint(page_action);
        break;
      }
      case ExtensionInstalledBubble::ANCHOR_OMNIBOX: {
        LocationBarViewMac* locationBarView =
            [window->cocoa_controller() locationBarBridge];
        arrowPoint = locationBarView->GetPageInfoBubblePoint();
        break;
      }
      case ExtensionInstalledBubble::ANCHOR_APP_MENU: {
        arrowPoint = getAppMenuButtonAnchorPoint();
        break;
      }
    }
  }
  return arrowPoint;
}

// Override -[BaseBubbleController showWindow:] to tweak bubble location and
// set up UI elements.
- (void)showWindow:(id)sender {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // Load nib and calculate height based on messages to be shown.
  NSWindow* window = [self initializeWindow];
  int newWindowHeight = [self calculateWindowHeight];
  [self.bubble setFrameSize:NSMakeSize(
      NSWidth([[window contentView] bounds]), newWindowHeight)];
  NSSize windowDelta = NSMakeSize(
      0, newWindowHeight - NSHeight([[window contentView] bounds]));
  windowDelta = [[window contentView] convertSize:windowDelta toView:nil];
  NSRect newFrame = [window frame];
  newFrame.size.height += windowDelta.height;
  [window setFrame:newFrame display:NO];

  // Now that we have resized the window, adjust y pos of the messages.
  [self setMessageFrames:newWindowHeight];

  // Find window origin, taking into account bubble size and arrow location.
  [self updateAnchorPosition];
  [super showWindow:sender];
}

// Finish nib loading, set arrow location and load icon into window.  This
// function is exposed for unit testing.
- (NSWindow*)initializeWindow {
  NSWindow* window = [self window];  // completes nib load

  if (installedBubble_ &&
      installedBubble_->anchor_position() ==
          ExtensionInstalledBubble::ANCHOR_OMNIBOX) {
    [self.bubble setArrowLocation:info_bubble::kTopLeft];
  } else {
    [self.bubble setArrowLocation:info_bubble::kTopRight];
  }

  if (type_ == extension_installed_bubble::kBundle)
    return window;

  // Set appropriate icon, resizing if necessary.
  if ([icon_ size].width > extension_installed_bubble::kIconSize) {
    [icon_ setSize:NSMakeSize(extension_installed_bubble::kIconSize,
                              extension_installed_bubble::kIconSize)];
  }
  [iconImage_ setImage:icon_];
  [iconImage_ setNeedsDisplay:YES];
  return window;
}

// Calculate the height of each install message, resizing messages in their
// frames to fit window width.  Return the new window height, based on the
// total of all message heights.
- (int)calculateWindowHeight {
  // Adjust the window height to reflect the sum height of all messages
  // and vertical padding.
  // If there's few enough messages, the icon area may be larger than the
  // messages.
  int contentColumnHeight =
      2 * extension_installed_bubble::kOuterVerticalMargin;
  int iconColumnHeight = 2 * extension_installed_bubble::kOuterVerticalMargin +
                         NSHeight([iconImage_ frame]);

  // If type is bundle, list the extensions that were installed and those that
  // failed.
  if (type_ == extension_installed_bubble::kBundle) {
    NSInteger installedListHeight =
        [self addExtensionList:installedHeadingMsg_
                     itemsView:installedItemsView_
                         state:BundleInstaller::Item::STATE_INSTALLED];

    NSInteger failedListHeight =
        [self addExtensionList:failedHeadingMsg_
                     itemsView:failedItemsView_
                         state:BundleInstaller::Item::STATE_FAILED];

    contentColumnHeight += installedListHeight + failedListHeight;

    // Put some space between the lists if both are present.
    if (installedListHeight > 0 && failedListHeight > 0)
      contentColumnHeight += extension_installed_bubble::kInnerVerticalMargin;

    return std::max(contentColumnHeight, iconColumnHeight);
  }

  CGFloat syncPromoHeight = 0;
  if (installedBubble_->options() & ExtensionInstalledBubble::SIGN_IN_PROMO) {
    signin_metrics::AccessPoint accessPoint =
       signin_metrics::AccessPoint::ACCESS_POINT_EXTENSION_INSTALL_BUBBLE;
    syncPromoController_.reset(
        [[BubbleSyncPromoController alloc]
            initWithBrowser:browser_
              promoStringId:IDS_EXTENSION_INSTALLED_SYNC_PROMO_NEW
               linkStringId:IDS_EXTENSION_INSTALLED_SYNC_PROMO_LINK_NEW
                accessPoint:accessPoint]);
    [promoContainer_ addSubview:[syncPromoController_ view]];

    // Resize the sync promo and its placeholder.
    NSRect syncPromoPlaceholderFrame = [promoContainer_ frame];
    CGFloat windowWidth = NSWidth([[self bubble] frame]);
    syncPromoPlaceholderFrame.size.width = windowWidth;
    syncPromoHeight =
        [syncPromoController_ preferredHeightForWidth:windowWidth];
    syncPromoPlaceholderFrame.size.height = syncPromoHeight;

    [promoContainer_ setFrame:syncPromoPlaceholderFrame];
    [[syncPromoController_ view] setFrame:syncPromoPlaceholderFrame];
  } else {
    [promoContainer_ setHidden:YES];
  }

  // First part of extension installed message, the heading.
  base::string16 extension_name =
      base::UTF8ToUTF16([self extension]->name().c_str());
  base::i18n::AdjustStringForLocaleDirection(&extension_name);
  [heading_ setStringValue:l10n_util::GetNSStringF(
      IDS_EXTENSION_INSTALLED_HEADING, extension_name)];
  [GTMUILocalizerAndLayoutTweaker
      sizeToFitFixedWidthTextField:heading_];
  contentColumnHeight += NSHeight([heading_ frame]);

  if (installedBubble_->options() & ExtensionInstalledBubble::HOW_TO_USE) {
    [howToUse_ setStringValue:base::SysUTF16ToNSString(
         installedBubble_->GetHowToUseDescription())];
    [howToUse_ setHidden:NO];
    [[howToUse_ cell]
        setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
    [GTMUILocalizerAndLayoutTweaker
        sizeToFitFixedWidthTextField:howToUse_];
    contentColumnHeight += NSHeight([howToUse_ frame]) +
        extension_installed_bubble::kInnerVerticalMargin;
  }

  // If type is app, hide howToManage_, and include a "show me" link in the
  // bubble.
  if (type_ == extension_installed_bubble::kApp) {
    [howToManage_ setHidden:YES];
    [appShortcutLink_ setHidden:NO];
    contentColumnHeight += 2 * extension_installed_bubble::kInnerVerticalMargin;
    contentColumnHeight += NSHeight([appShortcutLink_ frame]);
  } else if (installedBubble_->options() &
                 ExtensionInstalledBubble::HOW_TO_MANAGE) {
    // Second part of extension installed message.
    [[howToManage_ cell]
        setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
    [GTMUILocalizerAndLayoutTweaker
        sizeToFitFixedWidthTextField:howToManage_];
    contentColumnHeight += NSHeight([howToManage_ frame]) +
        extension_installed_bubble::kInnerVerticalMargin;
  } else {
    [howToManage_ setHidden:YES];
  }

  // Sync sign-in promo, if any.
  if (syncPromoHeight > 0) {
    // The sync promo goes at the bottom of the window and includes its own
    // bottom margin. Thus, we subtract off the one of the outer margins, and
    // apply it to both the icon area and content area.
    int syncPromoDelta = extension_installed_bubble::kInnerVerticalMargin +
                         syncPromoHeight -
                         extension_installed_bubble::kOuterVerticalMargin;
    contentColumnHeight += syncPromoDelta;
    iconColumnHeight += syncPromoDelta;
  }

  if (installedBubble_->options() & ExtensionInstalledBubble::SHOW_KEYBINDING) {
    [manageShortcutLink_ setHidden:NO];
    [[manageShortcutLink_ cell]
        setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
    [[manageShortcutLink_ cell]
        setTextColor:skia::SkColorToCalibratedNSColor(
            chrome_style::GetLinkColor())];
    [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageShortcutLink_];
    contentColumnHeight += extension_installed_bubble::kInnerVerticalMargin;
    contentColumnHeight += NSHeight([manageShortcutLink_ frame]);
  }

  return std::max(contentColumnHeight, iconColumnHeight);
}

- (NSInteger)addExtensionList:(NSTextField*)headingMsg
                    itemsView:(NSView*)itemsView
                        state:(BundleInstaller::Item::State)state {
  base::string16 heading = bundle_->GetHeadingTextFor(state);
  bool hidden = heading.empty();
  [headingMsg setHidden:hidden];
  [itemsView setHidden:hidden];
  if (hidden)
    return 0;

  [headingMsg setStringValue:base::SysUTF16ToNSString(heading)];
  [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:headingMsg];

  CGFloat height =
      PopulateBundleItemsList(bundle_->GetItemsWithState(state), itemsView);

  NSRect frame = [itemsView frame];
  frame.size.height = height;
  [itemsView setFrame:frame];

  return NSHeight([headingMsg frame]) +
      extension_installed_bubble::kInnerVerticalMargin +
      NSHeight([itemsView frame]);
}

// Adjust y-position of messages to sit properly in new window height.
- (void)setMessageFrames:(int)newWindowHeight {
  if (type_ == extension_installed_bubble::kBundle) {
    // Layout the messages from the bottom up.
    NSView* msgs[] = { failedItemsView_, failedHeadingMsg_,
                       installedItemsView_, installedHeadingMsg_ };
    NSInteger offsetFromBottom = 0;
    BOOL isFirstVisible = YES;
    for (size_t i = 0; i < arraysize(msgs); ++i) {
      if ([msgs[i] isHidden])
        continue;

      NSRect frame = [msgs[i] frame];
      NSInteger margin = isFirstVisible ?
          extension_installed_bubble::kOuterVerticalMargin :
          extension_installed_bubble::kInnerVerticalMargin;

      frame.origin.y = offsetFromBottom + margin;
      [msgs[i] setFrame:frame];
      offsetFromBottom += NSHeight(frame) + margin;

      isFirstVisible = NO;
    }

    // Move the close button a bit to vertically align it with the heading.
    NSInteger closeButtonFudge = 1;
    NSRect frame = [closeButton_ frame];
    frame.origin.y = newWindowHeight - (NSHeight(frame) + closeButtonFudge +
         extension_installed_bubble::kOuterVerticalMargin);
    [closeButton_ setFrame:frame];

    return;
  }

  NSRect headingFrame = [heading_ frame];
  headingFrame.origin.y = newWindowHeight - (
      NSHeight(headingFrame) +
      extension_installed_bubble::kOuterVerticalMargin);
  [heading_ setFrame:headingFrame];
  int nextY = NSMinY(headingFrame);

  auto adjustView = [](NSView* view, int* nextY) {
    DCHECK(nextY);
    NSRect frame = [view frame];
    frame.origin.y = *nextY -
        (NSHeight(frame) + extension_installed_bubble::kInnerVerticalMargin);
    [view setFrame:frame];
    *nextY = NSMinY(frame);
  };

  if (installedBubble_->options() & ExtensionInstalledBubble::HOW_TO_USE)
    adjustView(howToUse_, &nextY);

  if (installedBubble_->options() & ExtensionInstalledBubble::HOW_TO_MANAGE)
    adjustView(howToManage_, &nextY);

  if (installedBubble_->options() & ExtensionInstalledBubble::SHOW_KEYBINDING)
    adjustView(manageShortcutLink_, &nextY);

  if (installedBubble_->options() & ExtensionInstalledBubble::SIGN_IN_PROMO) {
    // The sync promo goes at the bottom of the bubble, but that might be
    // different than directly below the previous content if the icon is larger
    // than the messages. Workaround by just always setting nextY to be at the
    // bottom.
    nextY = NSHeight([promoContainer_ frame]) +
            extension_installed_bubble::kInnerVerticalMargin;
    adjustView(promoContainer_, &nextY);
  }
}

- (void)updateAnchorPosition {
  self.anchorPoint = ui::ConvertPointFromWindowToScreen(
      self.parentWindow, [self calculateArrowPoint]);
}

- (IBAction)onManageShortcutClicked:(id)sender {
  DCHECK([self bubbleReference]);
  bool didClose = [self bubbleReference]->CloseBubble(BUBBLE_CLOSE_ACCEPTED);
  DCHECK(didClose);
  std::string configure_url = chrome::kChromeUIExtensionsURL;
  configure_url += chrome::kExtensionConfigureCommandsSubPage;
  chrome::NavigateParams params(chrome::GetSingletonTabNavigateParams(
      browser_, GURL(configure_url)));
  chrome::Navigate(&params);
}

- (IBAction)onAppShortcutClicked:(id)sender {
  scoped_ptr<extensions::ExtensionInstallUI> install_ui(
      extensions::CreateExtensionInstallUI(browser_->profile()));
  install_ui->OpenAppInstalledUI([self extension]->id());
}

- (void)doClose {
  installedBubble_ = nullptr;
  [self close];
}

@end