Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 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/ui/views/extensions/extension_install_dialog_view.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/basictypes.h"
     10 #include "base/command_line.h"
     11 #include "base/compiler_specific.h"
     12 #include "base/i18n/rtl.h"
     13 #include "base/metrics/histogram.h"
     14 #include "base/strings/string_util.h"
     15 #include "base/strings/utf_string_conversions.h"
     16 #include "chrome/browser/browser_process.h"
     17 #include "chrome/browser/extensions/api/experience_sampling_private/experience_sampling.h"
     18 #include "chrome/browser/extensions/bundle_installer.h"
     19 #include "chrome/browser/extensions/extension_install_prompt_experiment.h"
     20 #include "chrome/browser/profiles/profile.h"
     21 #include "chrome/browser/profiles/profile_manager.h"
     22 #include "chrome/browser/ui/views/constrained_window_views.h"
     23 #include "chrome/common/extensions/extension_constants.h"
     24 #include "chrome/grit/generated_resources.h"
     25 #include "chrome/installer/util/browser_distribution.h"
     26 #include "content/public/browser/page_navigator.h"
     27 #include "content/public/browser/web_contents.h"
     28 #include "extensions/common/extension.h"
     29 #include "extensions/common/extension_urls.h"
     30 #include "grit/theme_resources.h"
     31 #include "ui/base/l10n/l10n_util.h"
     32 #include "ui/base/resource/resource_bundle.h"
     33 #include "ui/gfx/text_utils.h"
     34 #include "ui/views/background.h"
     35 #include "ui/views/border.h"
     36 #include "ui/views/controls/button/checkbox.h"
     37 #include "ui/views/controls/button/image_button.h"
     38 #include "ui/views/controls/button/label_button.h"
     39 #include "ui/views/controls/image_view.h"
     40 #include "ui/views/controls/label.h"
     41 #include "ui/views/controls/link.h"
     42 #include "ui/views/controls/scroll_view.h"
     43 #include "ui/views/controls/separator.h"
     44 #include "ui/views/layout/box_layout.h"
     45 #include "ui/views/layout/grid_layout.h"
     46 #include "ui/views/layout/layout_constants.h"
     47 #include "ui/views/widget/widget.h"
     48 #include "ui/views/window/dialog_client_view.h"
     49 
     50 using content::OpenURLParams;
     51 using content::Referrer;
     52 using extensions::BundleInstaller;
     53 using extensions::ExperienceSamplingEvent;
     54 
     55 namespace {
     56 
     57 // Width of the bullet column in BulletedView.
     58 const int kBulletWidth = 20;
     59 
     60 // Size of extension icon in top left of dialog.
     61 const int kIconSize = 64;
     62 
     63 // We offset the icon a little bit from the right edge of the dialog, to make it
     64 // align with the button below it.
     65 const int kIconOffset = 16;
     66 
     67 // The dialog will resize based on its content, but this sets a maximum height
     68 // before overflowing a scrollbar.
     69 const int kDialogMaxHeight = 300;
     70 
     71 // Width of the left column of the dialog when the extension requests
     72 // permissions.
     73 const int kPermissionsLeftColumnWidth = 250;
     74 
     75 // Width of the left column of the dialog when the extension requests no
     76 // permissions.
     77 const int kNoPermissionsLeftColumnWidth = 200;
     78 
     79 // Width of the left column for bundle install prompts. There's only one column
     80 // in this case, so make it wider than normal.
     81 const int kBundleLeftColumnWidth = 300;
     82 
     83 // Width of the left column for external install prompts. The text is long in
     84 // this case, so make it wider than normal.
     85 const int kExternalInstallLeftColumnWidth = 350;
     86 
     87 // Lighter color for labels.
     88 const SkColor kLighterLabelColor = SkColorSetRGB(0x99, 0x99, 0x99);
     89 
     90 // Represents an action on a clickable link created by the install prompt
     91 // experiment. This is used to group the actions in UMA histograms named
     92 // Extensions.InstallPromptExperiment.ShowDetails and
     93 // Extensions.InstallPromptExperiment.ShowPermissions.
     94 enum ExperimentLinkAction {
     95   LINK_SHOWN = 0,
     96   LINK_NOT_SHOWN,
     97   LINK_CLICKED,
     98   NUM_LINK_ACTIONS
     99 };
    100 
    101 void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) {
    102   views::View* parent = static_cast<views::View*>(data);
    103   views::ImageView* image_view = new views::ImageView();
    104   image_view->SetImage(*skia_image);
    105   parent->AddChildView(image_view);
    106 }
    107 
    108 // Creates a string for displaying |message| to the user. If it has to look
    109 // like a entry in a bullet point list, one is added.
    110 base::string16 PrepareForDisplay(const base::string16& message,
    111                                  bool bullet_point) {
    112   return bullet_point ? l10n_util::GetStringFUTF16(
    113       IDS_EXTENSION_PERMISSION_LINE,
    114       message) : message;
    115 }
    116 
    117 }  // namespace
    118 
    119 BulletedView::BulletedView(views::View* view) {
    120   views::GridLayout* layout = new views::GridLayout(this);
    121   SetLayoutManager(layout);
    122   views::ColumnSet* column_set = layout->AddColumnSet(0);
    123   column_set->AddColumn(views::GridLayout::CENTER,
    124                         views::GridLayout::LEADING,
    125                         0,
    126                         views::GridLayout::FIXED,
    127                         kBulletWidth,
    128                         0);
    129   column_set->AddColumn(views::GridLayout::LEADING,
    130                         views::GridLayout::LEADING,
    131                         0,
    132                         views::GridLayout::USE_PREF,
    133                         0,  // No fixed width.
    134                         0);
    135   layout->StartRow(0, 0);
    136   layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true)));
    137   layout->AddView(view);
    138 }
    139 
    140 CheckboxedView::CheckboxedView(views::View* view,
    141                                views::ButtonListener* listener) {
    142   views::GridLayout* layout = new views::GridLayout(this);
    143   SetLayoutManager(layout);
    144   views::ColumnSet* column_set = layout->AddColumnSet(0);
    145   column_set->AddColumn(views::GridLayout::LEADING,
    146                         views::GridLayout::LEADING,
    147                         0,
    148                         views::GridLayout::USE_PREF,
    149                         0,  // No fixed width.
    150                         0);
    151   column_set->AddColumn(views::GridLayout::LEADING,
    152                         views::GridLayout::LEADING,
    153                         0,
    154                         views::GridLayout::USE_PREF,
    155                         0,  // No fixed width.
    156                         0);
    157   layout->StartRow(0, 0);
    158   views::Checkbox* checkbox = new views::Checkbox(base::string16());
    159   checkbox->set_listener(listener);
    160   // Alignment needs to be explicitly set again here, otherwise the views are
    161   // not vertically centered.
    162   layout->AddView(checkbox, 1, 1,
    163                   views::GridLayout::LEADING, views::GridLayout::CENTER);
    164   layout->AddView(view, 1, 1,
    165                   views::GridLayout::LEADING, views::GridLayout::CENTER);
    166 }
    167 
    168 void ShowExtensionInstallDialogImpl(
    169     const ExtensionInstallPrompt::ShowParams& show_params,
    170     ExtensionInstallPrompt::Delegate* delegate,
    171     scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
    172   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    173   CreateBrowserModalDialogViews(
    174       new ExtensionInstallDialogView(show_params.navigator, delegate, prompt),
    175       show_params.parent_window)->Show();
    176 }
    177 
    178 CustomScrollableView::CustomScrollableView() {}
    179 CustomScrollableView::~CustomScrollableView() {}
    180 
    181 void CustomScrollableView::Layout() {
    182   SetBounds(x(), y(), width(), GetHeightForWidth(width()));
    183   views::View::Layout();
    184 }
    185 
    186 ExtensionInstallDialogView::ExtensionInstallDialogView(
    187     content::PageNavigator* navigator,
    188     ExtensionInstallPrompt::Delegate* delegate,
    189     scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
    190     : navigator_(navigator),
    191       delegate_(delegate),
    192       prompt_(prompt),
    193       scroll_view_(NULL),
    194       scrollable_(NULL),
    195       scrollable_header_only_(NULL),
    196       show_details_link_(NULL),
    197       checkbox_info_label_(NULL),
    198       unchecked_boxes_(0),
    199       handled_result_(false) {
    200   InitView();
    201 }
    202 
    203 ExtensionInstallDialogView::~ExtensionInstallDialogView() {
    204   if (!handled_result_)
    205     delegate_->InstallUIAbort(true);
    206 }
    207 
    208 void ExtensionInstallDialogView::InitView() {
    209   // Possible grid layouts without ExtensionPermissionDialog experiment:
    210   // Inline install
    211   //      w/ permissions                 no permissions
    212   // +--------------------+------+  +--------------+------+
    213   // | heading            | icon |  | heading      | icon |
    214   // +--------------------|      |  +--------------|      |
    215   // | rating             |      |  | rating       |      |
    216   // +--------------------|      |  +--------------+      |
    217   // | user_count         |      |  | user_count   |      |
    218   // +--------------------|      |  +--------------|      |
    219   // | store_link         |      |  | store_link   |      |
    220   // +--------------------+------+  +--------------+------+
    221   // |      separator            |
    222   // +--------------------+------+
    223   // | permissions_header |      |
    224   // +--------------------+------+
    225   // | permission1        |      |
    226   // +--------------------+------+
    227   // | permission2        |      |
    228   // +--------------------+------+
    229   //
    230   // Regular install
    231   // w/ permissions                     no permissions
    232   // +--------------------+------+  +--------------+------+
    233   // | heading            | icon |  | heading      | icon |
    234   // +--------------------|      |  +--------------+------+
    235   // | permissions_header |      |
    236   // +--------------------|      |
    237   // | permission1        |      |
    238   // +--------------------|      |
    239   // | permission2        |      |
    240   // +--------------------+------+
    241   //
    242   // If the ExtensionPermissionDialog is on, the layout is modified depending
    243   // on the experiment group. For text only experiment, a footer is added at the
    244   // bottom of the layouts. For others, inline details are added below some of
    245   // the permissions.
    246   //
    247   // Regular install w/ permissions and footer (experiment):
    248   // +--------------------+------+
    249   // | heading            | icon |
    250   // +--------------------|      |
    251   // | permissions_header |      |
    252   // +--------------------|      |
    253   // | permission1        |      |
    254   // +--------------------|      |
    255   // | permission2        |      |
    256   // +--------------------+------+
    257   // | footer text        |      |
    258   // +--------------------+------+
    259   //
    260   // Regular install w/ permissions and inline explanations (experiment):
    261   // +--------------------+------+
    262   // | heading            | icon |
    263   // +--------------------|      |
    264   // | permissions_header |      |
    265   // +--------------------|      |
    266   // | permission1        |      |
    267   // +--------------------|      |
    268   // | explanation1       |      |
    269   // +--------------------|      |
    270   // | permission2        |      |
    271   // +--------------------|      |
    272   // | explanation2       |      |
    273   // +--------------------+------+
    274   //
    275   // Regular install w/ permissions and inline explanations (experiment):
    276   // +--------------------+------+
    277   // | heading            | icon |
    278   // +--------------------|      |
    279   // | permissions_header |      |
    280   // +--------------------|      |
    281   // |checkbox|permission1|      |
    282   // +--------------------|      |
    283   // |checkbox|permission2|      |
    284   // +--------------------+------+
    285   //
    286   // Additionally, links or informational text is added to non-client areas of
    287   // the dialog depending on the experiment group.
    288 
    289   int left_column_width =
    290       (prompt_->ShouldShowPermissions() + prompt_->GetRetainedFileCount()) > 0
    291           ? kPermissionsLeftColumnWidth
    292           : kNoPermissionsLeftColumnWidth;
    293   if (is_bundle_install())
    294     left_column_width = kBundleLeftColumnWidth;
    295   if (is_external_install())
    296     left_column_width = kExternalInstallLeftColumnWidth;
    297 
    298   scroll_view_ = new views::ScrollView();
    299   scroll_view_->set_hide_horizontal_scrollbar(true);
    300   AddChildView(scroll_view_);
    301 
    302   int column_set_id = 0;
    303   // Create the full scrollable view which will contain all the information
    304   // including the permissions.
    305   scrollable_ = new CustomScrollableView();
    306   views::GridLayout* layout = CreateLayout(
    307       scrollable_, left_column_width, column_set_id, false);
    308   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    309 
    310   if (prompt_->ShouldShowPermissions() &&
    311       prompt_->experiment()->should_show_expandable_permission_list()) {
    312     // If the experiment should hide the permission list initially, create a
    313     // simple layout that contains only the header, extension name and icon.
    314     scrollable_header_only_ = new CustomScrollableView();
    315     CreateLayout(scrollable_header_only_, left_column_width,
    316                  column_set_id, true);
    317     scroll_view_->SetContents(scrollable_header_only_);
    318   } else {
    319     scroll_view_->SetContents(scrollable_);
    320   }
    321 
    322   int dialog_width = left_column_width + 2 * views::kPanelHorizMargin;
    323   if (!is_bundle_install())
    324     dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset;
    325 
    326   // Widen the dialog for experiment with checkboxes so that the information
    327   // label fits the area to the left of the buttons.
    328   if (prompt_->experiment()->show_checkboxes())
    329     dialog_width += 4 * views::kPanelHorizMargin;
    330 
    331   if (prompt_->has_webstore_data()) {
    332     layout->StartRow(0, column_set_id);
    333     views::View* rating = new views::View();
    334     rating->SetLayoutManager(new views::BoxLayout(
    335         views::BoxLayout::kHorizontal, 0, 0, 0));
    336     layout->AddView(rating);
    337     prompt_->AppendRatingStars(AddResourceIcon, rating);
    338 
    339     const gfx::FontList& small_font_list =
    340         rb.GetFontList(ui::ResourceBundle::SmallFont);
    341     views::Label* rating_count =
    342         new views::Label(prompt_->GetRatingCount(), small_font_list);
    343     // Add some space between the stars and the rating count.
    344     rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0));
    345     rating->AddChildView(rating_count);
    346 
    347     layout->StartRow(0, column_set_id);
    348     views::Label* user_count =
    349         new views::Label(prompt_->GetUserCount(), small_font_list);
    350     user_count->SetAutoColorReadabilityEnabled(false);
    351     user_count->SetEnabledColor(SK_ColorGRAY);
    352     layout->AddView(user_count);
    353 
    354     layout->StartRow(0, column_set_id);
    355     views::Link* store_link = new views::Link(
    356         l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK));
    357     store_link->SetFontList(small_font_list);
    358     store_link->set_listener(this);
    359     layout->AddView(store_link);
    360   }
    361 
    362   if (is_bundle_install()) {
    363     BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState(
    364         BundleInstaller::Item::STATE_PENDING);
    365     for (size_t i = 0; i < items.size(); ++i) {
    366       base::string16 extension_name =
    367           base::UTF8ToUTF16(items[i].localized_name);
    368       base::i18n::AdjustStringForLocaleDirection(&extension_name);
    369       layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    370       layout->StartRow(0, column_set_id);
    371       views::Label* extension_label = new views::Label(
    372           PrepareForDisplay(extension_name, true));
    373       extension_label->SetMultiLine(true);
    374       extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    375       extension_label->SizeToFit(left_column_width);
    376       layout->AddView(extension_label);
    377     }
    378   }
    379 
    380   bool has_permissions =
    381       prompt_->GetPermissionCount(
    382           ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS) > 0;
    383   if (prompt_->ShouldShowPermissions()) {
    384     AddPermissions(
    385         layout,
    386         rb,
    387         column_set_id,
    388         left_column_width,
    389         ExtensionInstallPrompt::PermissionsType::REGULAR_PERMISSIONS);
    390     AddPermissions(
    391         layout,
    392         rb,
    393         column_set_id,
    394         left_column_width,
    395         ExtensionInstallPrompt::PermissionsType::WITHHELD_PERMISSIONS);
    396     if (!has_permissions) {
    397       layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    398       layout->StartRow(0, column_set_id);
    399       views::Label* permission_label = new views::Label(
    400           l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS));
    401       permission_label->SetMultiLine(true);
    402       permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    403       permission_label->SizeToFit(left_column_width);
    404       layout->AddView(permission_label);
    405     }
    406   }
    407 
    408   if (prompt_->GetRetainedFileCount()) {
    409     // Slide in under the permissions, if there are any. If there are
    410     // either, the retained files prompt stretches all the way to the
    411     // right of the dialog. If there are no permissions, the retained
    412     // files prompt just takes up the left column.
    413     int space_for_files = left_column_width;
    414     if (has_permissions) {
    415       space_for_files += kIconSize;
    416       views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
    417       column_set->AddColumn(views::GridLayout::FILL,
    418                             views::GridLayout::FILL,
    419                             1,
    420                             views::GridLayout::USE_PREF,
    421                             0,  // no fixed width
    422                             space_for_files);
    423     }
    424 
    425     layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    426 
    427     layout->StartRow(0, column_set_id);
    428     views::Label* retained_files_header = NULL;
    429     retained_files_header =
    430         new views::Label(prompt_->GetRetainedFilesHeading());
    431     retained_files_header->SetMultiLine(true);
    432     retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    433     retained_files_header->SizeToFit(space_for_files);
    434     layout->AddView(retained_files_header);
    435 
    436     layout->StartRow(0, column_set_id);
    437     PermissionDetails details;
    438     for (size_t i = 0; i < prompt_->GetRetainedFileCount(); ++i)
    439       details.push_back(prompt_->GetRetainedFile(i));
    440     ExpandableContainerView* issue_advice_view =
    441         new ExpandableContainerView(
    442             this, base::string16(), details, space_for_files,
    443             false, true, false);
    444     layout->AddView(issue_advice_view);
    445   }
    446 
    447   DCHECK(prompt_->type() >= 0);
    448   UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type",
    449                             prompt_->type(),
    450                             ExtensionInstallPrompt::NUM_PROMPT_TYPES);
    451 
    452   if (prompt_->ShouldShowPermissions()) {
    453     if (prompt_->ShouldShowExplanationText()) {
    454       views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
    455       column_set->AddColumn(views::GridLayout::LEADING,
    456                             views::GridLayout::FILL,
    457                             1,
    458                             views::GridLayout::USE_PREF,
    459                             0,
    460                             0);
    461       // Add two rows of space so that the text stands out.
    462       layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing);
    463 
    464       layout->StartRow(0, column_set_id);
    465       views::Label* explanation =
    466           new views::Label(prompt_->experiment()->GetExplanationText());
    467       explanation->SetMultiLine(true);
    468       explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    469       explanation->SizeToFit(left_column_width + kIconSize);
    470       layout->AddView(explanation);
    471     }
    472 
    473     if (prompt_->experiment()->should_show_expandable_permission_list() ||
    474         (prompt_->experiment()->show_details_link() &&
    475          prompt_->experiment()->should_show_inline_explanations() &&
    476          !inline_explanations_.empty())) {
    477       // Don't show the "Show details" link if there are retained
    478       // files.  These have their own "Show details" links and having
    479       // multiple levels of links is confusing.
    480       if (prompt_->GetRetainedFileCount() == 0) {
    481         int text_id =
    482             prompt_->experiment()->should_show_expandable_permission_list()
    483                 ? IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS
    484                 : IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS;
    485         show_details_link_ = new views::Link(
    486             l10n_util::GetStringUTF16(text_id));
    487         show_details_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    488         show_details_link_->set_listener(this);
    489         UpdateLinkActionHistogram(LINK_SHOWN);
    490       } else {
    491         UpdateLinkActionHistogram(LINK_NOT_SHOWN);
    492       }
    493     }
    494 
    495     if (prompt_->experiment()->show_checkboxes()) {
    496       checkbox_info_label_ = new views::Label(
    497           l10n_util::GetStringUTF16(
    498               IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO));
    499       checkbox_info_label_->SetMultiLine(true);
    500       checkbox_info_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    501       checkbox_info_label_->SetAutoColorReadabilityEnabled(false);
    502       checkbox_info_label_->SetEnabledColor(kLighterLabelColor);
    503     }
    504   }
    505 
    506   gfx::Size scrollable_size = scrollable_->GetPreferredSize();
    507   scrollable_->SetBoundsRect(gfx::Rect(scrollable_size));
    508   dialog_size_ = gfx::Size(
    509       dialog_width,
    510       std::min(scrollable_size.height(), kDialogMaxHeight));
    511 
    512   if (scrollable_header_only_) {
    513     gfx::Size header_only_size = scrollable_header_only_->GetPreferredSize();
    514     scrollable_header_only_->SetBoundsRect(gfx::Rect(header_only_size));
    515     dialog_size_ = gfx::Size(
    516         dialog_width, std::min(header_only_size.height(), kDialogMaxHeight));
    517   }
    518 
    519   std::string event_name = ExperienceSamplingEvent::kExtensionInstallDialog;
    520   event_name.append(
    521       ExtensionInstallPrompt::PromptTypeToString(prompt_->type()));
    522   sampling_event_ = ExperienceSamplingEvent::Create(event_name);
    523 }
    524 
    525 bool ExtensionInstallDialogView::AddPermissions(
    526     views::GridLayout* layout,
    527     ui::ResourceBundle& rb,
    528     int column_set_id,
    529     int left_column_width,
    530     ExtensionInstallPrompt::PermissionsType perm_type) {
    531   if (prompt_->GetPermissionCount(perm_type) == 0)
    532     return false;
    533 
    534   layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    535   if (is_inline_install()) {
    536     layout->StartRow(0, column_set_id);
    537     layout->AddView(new views::Separator(views::Separator::HORIZONTAL),
    538                     3,
    539                     1,
    540                     views::GridLayout::FILL,
    541                     views::GridLayout::FILL);
    542     layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    543   }
    544 
    545   layout->StartRow(0, column_set_id);
    546   views::Label* permissions_header = NULL;
    547   if (is_bundle_install()) {
    548     // We need to pass the FontList in the constructor, rather than calling
    549     // SetFontList later, because otherwise SizeToFit mis-judges the width
    550     // of the line.
    551     permissions_header =
    552         new views::Label(prompt_->GetPermissionsHeading(perm_type),
    553                          rb.GetFontList(ui::ResourceBundle::MediumFont));
    554   } else {
    555     permissions_header =
    556         new views::Label(prompt_->GetPermissionsHeading(perm_type));
    557   }
    558   permissions_header->SetMultiLine(true);
    559   permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    560   permissions_header->SizeToFit(left_column_width);
    561   layout->AddView(permissions_header);
    562 
    563   for (size_t i = 0; i < prompt_->GetPermissionCount(perm_type); ++i) {
    564     layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    565     layout->StartRow(0, column_set_id);
    566     views::Label* permission_label =
    567         new views::Label(prompt_->GetPermission(i, perm_type));
    568 
    569     const SkColor kTextHighlight = SK_ColorRED;
    570     const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3);
    571     if (prompt_->experiment()->ShouldHighlightText(
    572             prompt_->GetPermission(i, perm_type))) {
    573       permission_label->SetAutoColorReadabilityEnabled(false);
    574       permission_label->SetEnabledColor(kTextHighlight);
    575     } else if (prompt_->experiment()->ShouldHighlightBackground(
    576                    prompt_->GetPermission(i, perm_type))) {
    577       permission_label->SetLineHeight(18);
    578       permission_label->set_background(
    579           views::Background::CreateSolidBackground(kBackgroundHighlight));
    580     }
    581 
    582     permission_label->SetMultiLine(true);
    583     permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    584 
    585     if (prompt_->experiment()->show_checkboxes()) {
    586       permission_label->SizeToFit(left_column_width);
    587       layout->AddView(new CheckboxedView(permission_label, this));
    588       ++unchecked_boxes_;
    589     } else {
    590       permission_label->SizeToFit(left_column_width - kBulletWidth);
    591       layout->AddView(new BulletedView(permission_label));
    592     }
    593 
    594     // If we have more details to provide, show them in collapsed form.
    595     if (!prompt_->GetPermissionsDetails(i, perm_type).empty()) {
    596       layout->StartRow(0, column_set_id);
    597       PermissionDetails details;
    598       details.push_back(PrepareForDisplay(
    599           prompt_->GetPermissionsDetails(i, perm_type), false));
    600       ExpandableContainerView* details_container =
    601           new ExpandableContainerView(this,
    602                                       base::string16(),
    603                                       details,
    604                                       left_column_width,
    605                                       true,
    606                                       true,
    607                                       false);
    608       layout->AddView(details_container);
    609     }
    610 
    611     if (prompt_->experiment()->should_show_inline_explanations()) {
    612       base::string16 explanation = prompt_->experiment()->GetInlineExplanation(
    613           prompt_->GetPermission(i, perm_type));
    614       if (!explanation.empty()) {
    615         PermissionDetails details;
    616         details.push_back(explanation);
    617         ExpandableContainerView* container =
    618             new ExpandableContainerView(this,
    619                                         base::string16(),
    620                                         details,
    621                                         left_column_width,
    622                                         false,
    623                                         false,
    624                                         true);
    625         // Inline explanations are expanded by default if there is
    626         // no "Show details" link.
    627         if (!prompt_->experiment()->show_details_link())
    628           container->ExpandWithoutAnimation();
    629         layout->StartRow(0, column_set_id);
    630         layout->AddView(container);
    631         inline_explanations_.push_back(container);
    632       }
    633     }
    634   }
    635   return true;
    636 }
    637 
    638 views::GridLayout* ExtensionInstallDialogView::CreateLayout(
    639     views::View* parent,
    640     int left_column_width,
    641     int column_set_id,
    642     bool single_detail_row) const {
    643   views::GridLayout* layout = views::GridLayout::CreatePanel(parent);
    644   parent->SetLayoutManager(layout);
    645 
    646   views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
    647   column_set->AddColumn(views::GridLayout::LEADING,
    648                         views::GridLayout::FILL,
    649                         0,  // no resizing
    650                         views::GridLayout::USE_PREF,
    651                         0,  // no fixed width
    652                         left_column_width);
    653   if (!is_bundle_install()) {
    654     column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
    655     column_set->AddColumn(views::GridLayout::TRAILING,
    656                           views::GridLayout::LEADING,
    657                           0,  // no resizing
    658                           views::GridLayout::USE_PREF,
    659                           0,  // no fixed width
    660                           kIconSize);
    661   }
    662 
    663   layout->StartRow(0, column_set_id);
    664 
    665   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    666 
    667   views::Label* heading = new views::Label(
    668       prompt_->GetHeading(), rb.GetFontList(ui::ResourceBundle::MediumFont));
    669   heading->SetMultiLine(true);
    670   heading->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    671   heading->SizeToFit(left_column_width);
    672   layout->AddView(heading);
    673 
    674   if (!is_bundle_install()) {
    675     // Scale down to icon size, but allow smaller icons (don't scale up).
    676     const gfx::ImageSkia* image = prompt_->icon().ToImageSkia();
    677     gfx::Size size(image->width(), image->height());
    678     if (size.width() > kIconSize || size.height() > kIconSize)
    679       size = gfx::Size(kIconSize, kIconSize);
    680     views::ImageView* icon = new views::ImageView();
    681     icon->SetImageSize(size);
    682     icon->SetImage(*image);
    683     icon->SetHorizontalAlignment(views::ImageView::CENTER);
    684     icon->SetVerticalAlignment(views::ImageView::CENTER);
    685     if (single_detail_row) {
    686       layout->AddView(icon);
    687     } else {
    688       int icon_row_span = 1;
    689       if (is_inline_install()) {
    690         // Also span the rating, user_count and store_link rows.
    691         icon_row_span = 4;
    692       } else if (prompt_->ShouldShowPermissions()) {
    693         size_t permission_count = prompt_->GetPermissionCount(
    694             ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS);
    695         // Also span the permission header and each of the permission rows (all
    696         // have a padding row above it). This also works for the 'no special
    697         // permissions' case.
    698         icon_row_span = 3 + permission_count * 2;
    699       } else if (prompt_->GetRetainedFileCount()) {
    700         // Also span the permission header and the retained files container.
    701         icon_row_span = 4;
    702       }
    703       layout->AddView(icon, 1, icon_row_span);
    704     }
    705   }
    706   return layout;
    707 }
    708 
    709 void ExtensionInstallDialogView::ContentsChanged() {
    710   Layout();
    711 }
    712 
    713 void ExtensionInstallDialogView::ViewHierarchyChanged(
    714     const ViewHierarchyChangedDetails& details) {
    715   // Since we want the links to show up in the same visual row as the accept
    716   // and cancel buttons, which is provided by the framework, we must add the
    717   // buttons to the non-client view, which is the parent of this view.
    718   // Similarly, when we're removed from the view hierarchy, we must take care
    719   // to clean up those items as well.
    720   if (details.child == this) {
    721     if (details.is_add) {
    722       if (show_details_link_)
    723         details.parent->AddChildView(show_details_link_);
    724       if (checkbox_info_label_)
    725         details.parent->AddChildView(checkbox_info_label_);
    726     } else {
    727       if (show_details_link_)
    728         details.parent->RemoveChildView(show_details_link_);
    729       if (checkbox_info_label_)
    730         details.parent->RemoveChildView(checkbox_info_label_);
    731     }
    732   }
    733 }
    734 
    735 int ExtensionInstallDialogView::GetDialogButtons() const {
    736   int buttons = prompt_->GetDialogButtons();
    737   // Simply having just an OK button is *not* supported. See comment on function
    738   // GetDialogButtons in dialog_delegate.h for reasons.
    739   DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0);
    740   return buttons;
    741 }
    742 
    743 base::string16 ExtensionInstallDialogView::GetDialogButtonLabel(
    744     ui::DialogButton button) const {
    745   switch (button) {
    746     case ui::DIALOG_BUTTON_OK:
    747       return prompt_->GetAcceptButtonLabel();
    748     case ui::DIALOG_BUTTON_CANCEL:
    749       return prompt_->HasAbortButtonLabel()
    750                  ? prompt_->GetAbortButtonLabel()
    751                  : l10n_util::GetStringUTF16(IDS_CANCEL);
    752     default:
    753       NOTREACHED();
    754       return base::string16();
    755   }
    756 }
    757 
    758 int ExtensionInstallDialogView::GetDefaultDialogButton() const {
    759   return ui::DIALOG_BUTTON_CANCEL;
    760 }
    761 
    762 bool ExtensionInstallDialogView::Cancel() {
    763   if (handled_result_)
    764     return true;
    765 
    766   handled_result_ = true;
    767   UpdateInstallResultHistogram(false);
    768   if (sampling_event_)
    769     sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kDeny);
    770   delegate_->InstallUIAbort(true);
    771   return true;
    772 }
    773 
    774 bool ExtensionInstallDialogView::Accept() {
    775   DCHECK(!handled_result_);
    776 
    777   handled_result_ = true;
    778   UpdateInstallResultHistogram(true);
    779   if (sampling_event_)
    780     sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kProceed);
    781   delegate_->InstallUIProceed();
    782   return true;
    783 }
    784 
    785 ui::ModalType ExtensionInstallDialogView::GetModalType() const {
    786   return ui::MODAL_TYPE_WINDOW;
    787 }
    788 
    789 base::string16 ExtensionInstallDialogView::GetWindowTitle() const {
    790   return prompt_->GetDialogTitle();
    791 }
    792 
    793 void ExtensionInstallDialogView::LinkClicked(views::Link* source,
    794                                              int event_flags) {
    795   if (source == show_details_link_) {
    796     UpdateLinkActionHistogram(LINK_CLICKED);
    797     // Show details link is used to either reveal whole permission list or to
    798     // reveal inline explanations.
    799     if (prompt_->experiment()->should_show_expandable_permission_list()) {
    800       gfx::Rect bounds = GetWidget()->GetWindowBoundsInScreen();
    801       int spacing = bounds.height() -
    802           scrollable_header_only_->GetPreferredSize().height();
    803       int content_height = std::min(scrollable_->GetPreferredSize().height(),
    804                                     kDialogMaxHeight);
    805       bounds.set_height(spacing + content_height);
    806       scroll_view_->SetContents(scrollable_);
    807       GetWidget()->SetBoundsConstrained(bounds);
    808       ContentsChanged();
    809     } else {
    810       ToggleInlineExplanations();
    811     }
    812     show_details_link_->SetVisible(false);
    813   } else {
    814     GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
    815                    prompt_->extension()->id());
    816     OpenURLParams params(
    817         store_url, Referrer(), NEW_FOREGROUND_TAB,
    818         ui::PAGE_TRANSITION_LINK,
    819         false);
    820     navigator_->OpenURL(params);
    821     GetWidget()->Close();
    822   }
    823 }
    824 
    825 void ExtensionInstallDialogView::ToggleInlineExplanations() {
    826   for (InlineExplanations::iterator it = inline_explanations_.begin();
    827       it != inline_explanations_.end(); ++it)
    828     (*it)->ToggleDetailLevel();
    829 }
    830 
    831 void ExtensionInstallDialogView::Layout() {
    832   scroll_view_->SetBounds(0, 0, width(), height());
    833 
    834   if (show_details_link_ || checkbox_info_label_) {
    835     views::LabelButton* cancel_button = GetDialogClientView()->cancel_button();
    836     gfx::Rect parent_bounds = parent()->GetContentsBounds();
    837     // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to
    838     // align the link horizontally with the left side of the contents of the
    839     // layout, put a horizontal margin with this amount.
    840     const int horizontal_margin = views::kButtonHEdgeMarginNew;
    841     const int vertical_margin = views::kButtonVEdgeMarginNew;
    842     int y_buttons = parent_bounds.bottom() -
    843         cancel_button->GetPreferredSize().height() - vertical_margin;
    844     int max_width = dialog_size_.width() - cancel_button->width() * 2 -
    845         horizontal_margin * 2 - views::kRelatedButtonHSpacing;
    846     if (show_details_link_) {
    847       gfx::Size link_size = show_details_link_->GetPreferredSize();
    848       show_details_link_->SetBounds(
    849           horizontal_margin,
    850           y_buttons + (cancel_button->height() - link_size.height()) / 2,
    851           link_size.width(), link_size.height());
    852     }
    853     if (checkbox_info_label_) {
    854       gfx::Size label_size = checkbox_info_label_->GetPreferredSize();
    855       checkbox_info_label_->SetBounds(
    856           horizontal_margin,
    857           y_buttons + (cancel_button->height() - label_size.height()) / 2,
    858           label_size.width(), label_size.height());
    859       checkbox_info_label_->SizeToFit(max_width);
    860     }
    861   }
    862   // Disable accept button if there are unchecked boxes and
    863   // the experiment is on.
    864   if (prompt_->experiment()->show_checkboxes())
    865     GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
    866 
    867   DialogDelegateView::Layout();
    868 }
    869 
    870 gfx::Size ExtensionInstallDialogView::GetPreferredSize() const {
    871   return dialog_size_;
    872 }
    873 
    874 void ExtensionInstallDialogView::ButtonPressed(views::Button* sender,
    875                                                const ui::Event& event) {
    876   if (std::string(views::Checkbox::kViewClassName) == sender->GetClassName()) {
    877     views::Checkbox* checkbox = static_cast<views::Checkbox*>(sender);
    878     if (checkbox->checked())
    879       --unchecked_boxes_;
    880     else
    881       ++unchecked_boxes_;
    882 
    883     GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
    884     checkbox_info_label_->SetVisible(unchecked_boxes_ > 0);
    885   }
    886 }
    887 
    888 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted)
    889     const {
    890   if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT)
    891     UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted);
    892 }
    893 
    894 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type)
    895     const {
    896   if (prompt_->experiment()->should_show_expandable_permission_list()) {
    897     // The clickable link in the UI is "Show Permissions".
    898     UMA_HISTOGRAM_ENUMERATION(
    899         "Extensions.InstallPromptExperiment.ShowPermissions",
    900         action_type,
    901         NUM_LINK_ACTIONS);
    902   } else {
    903     // The clickable link in the UI is "Show Details".
    904     UMA_HISTOGRAM_ENUMERATION(
    905         "Extensions.InstallPromptExperiment.ShowDetails",
    906         action_type,
    907         NUM_LINK_ACTIONS);
    908   }
    909 }
    910 
    911 // ExpandableContainerView::DetailsView ----------------------------------------
    912 
    913 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space,
    914                                                   bool parent_bulleted,
    915                                                   bool lighter_color)
    916     : layout_(new views::GridLayout(this)),
    917       state_(0),
    918       lighter_color_(lighter_color) {
    919   SetLayoutManager(layout_);
    920   views::ColumnSet* column_set = layout_->AddColumnSet(0);
    921   // If the parent is using bullets for its items, then a padding of one unit
    922   // will make the child item (which has no bullet) look like a sibling of its
    923   // parent. Therefore increase the indentation by one more unit to show that it
    924   // is in fact a child item (with no missing bullet) and not a sibling.
    925   int padding =
    926       views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1);
    927   column_set->AddPaddingColumn(0, padding);
    928   column_set->AddColumn(views::GridLayout::LEADING,
    929                         views::GridLayout::LEADING,
    930                         0,
    931                         views::GridLayout::FIXED,
    932                         horizontal_space - padding,
    933                         0);
    934 }
    935 
    936 void ExpandableContainerView::DetailsView::AddDetail(
    937     const base::string16& detail) {
    938   layout_->StartRowWithPadding(0, 0,
    939                                0, views::kRelatedControlSmallVerticalSpacing);
    940   views::Label* detail_label =
    941       new views::Label(PrepareForDisplay(detail, false));
    942   detail_label->SetMultiLine(true);
    943   detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    944   if (lighter_color_) {
    945     detail_label->SetEnabledColor(kLighterLabelColor);
    946     detail_label->SetAutoColorReadabilityEnabled(false);
    947   }
    948   layout_->AddView(detail_label);
    949 }
    950 
    951 gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const {
    952   gfx::Size size = views::View::GetPreferredSize();
    953   return gfx::Size(size.width(), size.height() * state_);
    954 }
    955 
    956 void ExpandableContainerView::DetailsView::AnimateToState(double state) {
    957   state_ = state;
    958   PreferredSizeChanged();
    959   SchedulePaint();
    960 }
    961 
    962 // ExpandableContainerView -----------------------------------------------------
    963 
    964 ExpandableContainerView::ExpandableContainerView(
    965     ExtensionInstallDialogView* owner,
    966     const base::string16& description,
    967     const PermissionDetails& details,
    968     int horizontal_space,
    969     bool parent_bulleted,
    970     bool show_expand_link,
    971     bool lighter_color_details)
    972     : owner_(owner),
    973       details_view_(NULL),
    974       more_details_(NULL),
    975       slide_animation_(this),
    976       arrow_toggle_(NULL),
    977       expanded_(false) {
    978   views::GridLayout* layout = new views::GridLayout(this);
    979   SetLayoutManager(layout);
    980   int column_set_id = 0;
    981   views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
    982   column_set->AddColumn(views::GridLayout::LEADING,
    983                         views::GridLayout::LEADING,
    984                         0,
    985                         views::GridLayout::USE_PREF,
    986                         0,
    987                         0);
    988   if (!description.empty()) {
    989     layout->StartRow(0, column_set_id);
    990 
    991     views::Label* description_label = new views::Label(description);
    992     description_label->SetMultiLine(true);
    993     description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    994     description_label->SizeToFit(horizontal_space);
    995     layout->AddView(new BulletedView(description_label));
    996   }
    997 
    998   if (details.empty())
    999     return;
   1000 
   1001   details_view_ = new DetailsView(horizontal_space, parent_bulleted,
   1002                                   lighter_color_details);
   1003 
   1004   layout->StartRow(0, column_set_id);
   1005   layout->AddView(details_view_);
   1006 
   1007   for (size_t i = 0; i < details.size(); ++i)
   1008     details_view_->AddDetail(details[i]);
   1009 
   1010   // TODO(meacer): Remove show_expand_link when the experiment is completed.
   1011   if (show_expand_link) {
   1012     views::Link* link = new views::Link(
   1013         l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
   1014 
   1015     // Make sure the link width column is as wide as needed for both Show and
   1016     // Hide details, so that the arrow doesn't shift horizontally when we
   1017     // toggle.
   1018     int link_col_width =
   1019         views::kRelatedControlHorizontalSpacing +
   1020         std::max(gfx::GetStringWidth(
   1021                      l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS),
   1022                      link->font_list()),
   1023                  gfx::GetStringWidth(
   1024                      l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS),
   1025                      link->font_list()));
   1026 
   1027     column_set = layout->AddColumnSet(++column_set_id);
   1028     // Padding to the left of the More Details column. If the parent is using
   1029     // bullets for its items, then a padding of one unit will make the child
   1030     // item (which has no bullet) look like a sibling of its parent. Therefore
   1031     // increase the indentation by one more unit to show that it is in fact a
   1032     // child item (with no missing bullet) and not a sibling.
   1033     column_set->AddPaddingColumn(
   1034         0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1));
   1035     // The More Details column.
   1036     column_set->AddColumn(views::GridLayout::LEADING,
   1037                           views::GridLayout::LEADING,
   1038                           0,
   1039                           views::GridLayout::FIXED,
   1040                           link_col_width,
   1041                           link_col_width);
   1042     // The Up/Down arrow column.
   1043     column_set->AddColumn(views::GridLayout::LEADING,
   1044                           views::GridLayout::LEADING,
   1045                           0,
   1046                           views::GridLayout::USE_PREF,
   1047                           0,
   1048                           0);
   1049 
   1050     // Add the More Details link.
   1051     layout->StartRow(0, column_set_id);
   1052     more_details_ = link;
   1053     more_details_->set_listener(this);
   1054     more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
   1055     layout->AddView(more_details_);
   1056 
   1057     // Add the arrow after the More Details link.
   1058     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
   1059     arrow_toggle_ = new views::ImageButton(this);
   1060     arrow_toggle_->SetImage(views::Button::STATE_NORMAL,
   1061                             rb.GetImageSkiaNamed(IDR_DOWN_ARROW));
   1062     layout->AddView(arrow_toggle_);
   1063   }
   1064 }
   1065 
   1066 ExpandableContainerView::~ExpandableContainerView() {
   1067 }
   1068 
   1069 void ExpandableContainerView::ButtonPressed(
   1070     views::Button* sender, const ui::Event& event) {
   1071   ToggleDetailLevel();
   1072 }
   1073 
   1074 void ExpandableContainerView::LinkClicked(
   1075     views::Link* source, int event_flags) {
   1076   ToggleDetailLevel();
   1077 }
   1078 
   1079 void ExpandableContainerView::AnimationProgressed(
   1080     const gfx::Animation* animation) {
   1081   DCHECK_EQ(&slide_animation_, animation);
   1082   if (details_view_)
   1083     details_view_->AnimateToState(animation->GetCurrentValue());
   1084 }
   1085 
   1086 void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) {
   1087   if (arrow_toggle_) {
   1088     if (animation->GetCurrentValue() != 0.0) {
   1089       arrow_toggle_->SetImage(
   1090           views::Button::STATE_NORMAL,
   1091           ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
   1092               IDR_UP_ARROW));
   1093     } else {
   1094       arrow_toggle_->SetImage(
   1095           views::Button::STATE_NORMAL,
   1096           ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
   1097               IDR_DOWN_ARROW));
   1098     }
   1099   }
   1100   if (more_details_) {
   1101     more_details_->SetText(expanded_ ?
   1102         l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) :
   1103         l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
   1104   }
   1105 }
   1106 
   1107 void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
   1108   owner_->ContentsChanged();
   1109 }
   1110 
   1111 void ExpandableContainerView::ToggleDetailLevel() {
   1112   expanded_ = !expanded_;
   1113 
   1114   if (slide_animation_.IsShowing())
   1115     slide_animation_.Hide();
   1116   else
   1117     slide_animation_.Show();
   1118 }
   1119 
   1120 void ExpandableContainerView::ExpandWithoutAnimation() {
   1121   expanded_ = true;
   1122   details_view_->AnimateToState(1.0);
   1123 }
   1124 
   1125 // static
   1126 ExtensionInstallPrompt::ShowDialogCallback
   1127 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
   1128   return base::Bind(&ShowExtensionInstallDialogImpl);
   1129 }
   1130