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
50using content::OpenURLParams;
51using content::Referrer;
52using extensions::BundleInstaller;
53using extensions::ExperienceSamplingEvent;
54
55namespace {
56
57// Width of the bullet column in BulletedView.
58const int kBulletWidth = 20;
59
60// Size of extension icon in top left of dialog.
61const 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.
65const int kIconOffset = 16;
66
67// The dialog will resize based on its content, but this sets a maximum height
68// before overflowing a scrollbar.
69const int kDialogMaxHeight = 300;
70
71// Width of the left column of the dialog when the extension requests
72// permissions.
73const int kPermissionsLeftColumnWidth = 250;
74
75// Width of the left column of the dialog when the extension requests no
76// permissions.
77const 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.
81const 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.
85const int kExternalInstallLeftColumnWidth = 350;
86
87// Lighter color for labels.
88const 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.
94enum ExperimentLinkAction {
95  LINK_SHOWN = 0,
96  LINK_NOT_SHOWN,
97  LINK_CLICKED,
98  NUM_LINK_ACTIONS
99};
100
101void 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.
110base::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
119BulletedView::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
140CheckboxedView::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
168void 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
178CustomScrollableView::CustomScrollableView() {}
179CustomScrollableView::~CustomScrollableView() {}
180
181void CustomScrollableView::Layout() {
182  SetBounds(x(), y(), width(), GetHeightForWidth(width()));
183  views::View::Layout();
184}
185
186ExtensionInstallDialogView::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
203ExtensionInstallDialogView::~ExtensionInstallDialogView() {
204  if (!handled_result_)
205    delegate_->InstallUIAbort(true);
206}
207
208void 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
525bool 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
638views::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
709void ExtensionInstallDialogView::ContentsChanged() {
710  Layout();
711}
712
713void 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
735int 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
743base::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
758int ExtensionInstallDialogView::GetDefaultDialogButton() const {
759  return ui::DIALOG_BUTTON_CANCEL;
760}
761
762bool 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
774bool 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
785ui::ModalType ExtensionInstallDialogView::GetModalType() const {
786  return ui::MODAL_TYPE_WINDOW;
787}
788
789base::string16 ExtensionInstallDialogView::GetWindowTitle() const {
790  return prompt_->GetDialogTitle();
791}
792
793void 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
825void ExtensionInstallDialogView::ToggleInlineExplanations() {
826  for (InlineExplanations::iterator it = inline_explanations_.begin();
827      it != inline_explanations_.end(); ++it)
828    (*it)->ToggleDetailLevel();
829}
830
831void 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
870gfx::Size ExtensionInstallDialogView::GetPreferredSize() const {
871  return dialog_size_;
872}
873
874void 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
888void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted)
889    const {
890  if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT)
891    UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted);
892}
893
894void 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
913ExpandableContainerView::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
936void 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
951gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const {
952  gfx::Size size = views::View::GetPreferredSize();
953  return gfx::Size(size.width(), size.height() * state_);
954}
955
956void ExpandableContainerView::DetailsView::AnimateToState(double state) {
957  state_ = state;
958  PreferredSizeChanged();
959  SchedulePaint();
960}
961
962// ExpandableContainerView -----------------------------------------------------
963
964ExpandableContainerView::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
1066ExpandableContainerView::~ExpandableContainerView() {
1067}
1068
1069void ExpandableContainerView::ButtonPressed(
1070    views::Button* sender, const ui::Event& event) {
1071  ToggleDetailLevel();
1072}
1073
1074void ExpandableContainerView::LinkClicked(
1075    views::Link* source, int event_flags) {
1076  ToggleDetailLevel();
1077}
1078
1079void 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
1086void 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
1107void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
1108  owner_->ContentsChanged();
1109}
1110
1111void ExpandableContainerView::ToggleDetailLevel() {
1112  expanded_ = !expanded_;
1113
1114  if (slide_animation_.IsShowing())
1115    slide_animation_.Hide();
1116  else
1117    slide_animation_.Show();
1118}
1119
1120void ExpandableContainerView::ExpandWithoutAnimation() {
1121  expanded_ = true;
1122  details_view_->AnimateToState(1.0);
1123}
1124
1125// static
1126ExtensionInstallPrompt::ShowDialogCallback
1127ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
1128  return base::Bind(&ShowExtensionInstallDialogImpl);
1129}
1130