1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/views/apps/app_info_dialog/app_info_summary_panel.h"
6
7#include <vector>
8
9#include "base/callback_forward.h"
10#include "base/files/file_util.h"
11#include "base/i18n/time_formatting.h"
12#include "base/logging.h"
13#include "base/strings/utf_string_conversions.h"
14#include "base/task_runner_util.h"
15#include "chrome/browser/extensions/extension_service.h"
16#include "chrome/browser/extensions/extension_util.h"
17#include "chrome/browser/extensions/launch_util.h"
18#include "chrome/browser/profiles/profile.h"
19#include "chrome/common/extensions/extension_constants.h"
20#include "chrome/grit/generated_resources.h"
21#include "content/public/browser/browser_thread.h"
22#include "extensions/browser/extension_prefs.h"
23#include "extensions/browser/extension_system.h"
24#include "extensions/common/extension.h"
25#include "extensions/common/manifest.h"
26#include "ui/base/l10n/l10n_util.h"
27#include "ui/base/models/combobox_model.h"
28#include "ui/base/text/bytes_formatting.h"
29#include "ui/views/controls/combobox/combobox.h"
30#include "ui/views/controls/label.h"
31#include "ui/views/layout/box_layout.h"
32#include "ui/views/layout/layout_constants.h"
33#include "ui/views/view.h"
34#include "ui/views/widget/widget.h"
35
36// A model for a combobox selecting the launch options for a hosted app.
37// Displays different options depending on the host OS.
38class LaunchOptionsComboboxModel : public ui::ComboboxModel {
39 public:
40  LaunchOptionsComboboxModel();
41  virtual ~LaunchOptionsComboboxModel();
42
43  extensions::LaunchType GetLaunchTypeAtIndex(int index) const;
44  int GetIndexForLaunchType(extensions::LaunchType launch_type) const;
45
46  // Overridden from ui::ComboboxModel:
47  virtual int GetItemCount() const OVERRIDE;
48  virtual base::string16 GetItemAt(int index) OVERRIDE;
49
50 private:
51  // A list of the launch types available in the combobox, in order.
52  std::vector<extensions::LaunchType> launch_types_;
53
54  // A list of the messages to display in the combobox, in order. The indexes in
55  // this list correspond to the indexes in launch_types_.
56  std::vector<base::string16> launch_type_messages_;
57};
58
59LaunchOptionsComboboxModel::LaunchOptionsComboboxModel() {
60  if (extensions::util::IsStreamlinedHostedAppsEnabled()) {
61    // Streamlined hosted apps can only toggle between LAUNCH_TYPE_WINDOW and
62    // LAUNCH_TYPE_REGULAR.
63    // TODO(sashab): Use a checkbox for this choice instead of combobox.
64    launch_types_.push_back(extensions::LAUNCH_TYPE_REGULAR);
65    launch_type_messages_.push_back(
66        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_TAB));
67
68    // Although LAUNCH_TYPE_WINDOW doesn't work on Mac, the streamlined hosted
69    // apps flag isn't available on Mac, so we must be on a non-Mac OS.
70    launch_types_.push_back(extensions::LAUNCH_TYPE_WINDOW);
71    launch_type_messages_.push_back(
72        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_WINDOW));
73  } else {
74    launch_types_.push_back(extensions::LAUNCH_TYPE_REGULAR);
75    launch_type_messages_.push_back(
76        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_REGULAR));
77
78    launch_types_.push_back(extensions::LAUNCH_TYPE_PINNED);
79    launch_type_messages_.push_back(
80        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_PINNED));
81
82#if defined(OS_MACOSX)
83    // Mac does not support standalone web app browser windows or maximize.
84    launch_types_.push_back(extensions::LAUNCH_TYPE_FULLSCREEN);
85    launch_type_messages_.push_back(
86        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_FULLSCREEN));
87#else
88    launch_types_.push_back(extensions::LAUNCH_TYPE_WINDOW);
89    launch_type_messages_.push_back(
90        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_WINDOW));
91
92    // Even though the launch type is Full Screen, it is more accurately
93    // described as Maximized in non-Mac OSs.
94    launch_types_.push_back(extensions::LAUNCH_TYPE_FULLSCREEN);
95    launch_type_messages_.push_back(
96        l10n_util::GetStringUTF16(IDS_APP_CONTEXT_MENU_OPEN_MAXIMIZED));
97#endif
98  }
99}
100
101LaunchOptionsComboboxModel::~LaunchOptionsComboboxModel() {
102}
103
104extensions::LaunchType LaunchOptionsComboboxModel::GetLaunchTypeAtIndex(
105    int index) const {
106  return launch_types_[index];
107}
108
109int LaunchOptionsComboboxModel::GetIndexForLaunchType(
110    extensions::LaunchType launch_type) const {
111  for (size_t i = 0; i < launch_types_.size(); i++) {
112    if (launch_types_[i] == launch_type) {
113      return i;
114    }
115  }
116  // If the requested launch type is not available, just select the first one.
117  LOG(WARNING) << "Unavailable launch type " << launch_type << " selected.";
118  return 0;
119}
120
121int LaunchOptionsComboboxModel::GetItemCount() const {
122  return launch_types_.size();
123}
124
125base::string16 LaunchOptionsComboboxModel::GetItemAt(int index) {
126  return launch_type_messages_[index];
127}
128
129AppInfoSummaryPanel::AppInfoSummaryPanel(Profile* profile,
130                                         const extensions::Extension* app)
131    : AppInfoPanel(profile, app),
132      description_heading_(NULL),
133      description_label_(NULL),
134      details_heading_(NULL),
135      size_title_(NULL),
136      size_value_(NULL),
137      version_title_(NULL),
138      version_value_(NULL),
139      installed_time_title_(NULL),
140      installed_time_value_(NULL),
141      last_run_time_title_(NULL),
142      last_run_time_value_(NULL),
143      launch_options_combobox_(NULL),
144      weak_ptr_factory_(this) {
145  // Create UI elements.
146  CreateDescriptionControl();
147  CreateDetailsControl();
148  CreateLaunchOptionControl();
149
150  // Layout elements.
151  SetLayoutManager(
152      new views::BoxLayout(views::BoxLayout::kVertical,
153                           0,
154                           0,
155                           views::kUnrelatedControlVerticalSpacing));
156
157  LayoutDescriptionControl();
158  LayoutDetailsControl();
159
160  if (launch_options_combobox_)
161    AddChildView(launch_options_combobox_);
162}
163
164AppInfoSummaryPanel::~AppInfoSummaryPanel() {
165  // Destroy view children before their models.
166  RemoveAllChildViews(true);
167}
168
169void AppInfoSummaryPanel::CreateDescriptionControl() {
170  if (!app_->description().empty()) {
171    const size_t kMaxLength = 400;
172
173    base::string16 text = base::UTF8ToUTF16(app_->description());
174    if (text.length() > kMaxLength) {
175      text = text.substr(0, kMaxLength);
176      text += base::ASCIIToUTF16(" ... ");
177    }
178
179    description_heading_ = CreateHeading(
180        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_DESCRIPTION_TITLE));
181    description_label_ = new views::Label(text);
182    description_label_->SetMultiLine(true);
183    description_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
184  }
185}
186
187void AppInfoSummaryPanel::CreateDetailsControl() {
188  // The size doesn't make sense for component apps.
189  if (app_->location() != extensions::Manifest::COMPONENT) {
190    size_title_ = new views::Label(
191        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_SIZE_LABEL));
192    size_title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
193
194    size_value_ = new views::Label(
195        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_SIZE_LOADING_LABEL));
196    size_value_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
197
198    StartCalculatingAppSize();
199  }
200
201  // The version doesn't make sense for bookmark apps.
202  if (!app_->from_bookmark()) {
203    // Display 'Version: Built-in' for component apps.
204    base::string16 version_str = base::ASCIIToUTF16(app_->VersionString());
205    if (app_->location() == extensions::Manifest::COMPONENT)
206      version_str = l10n_util::GetStringUTF16(
207          IDS_APPLICATION_INFO_VERSION_BUILT_IN_LABEL);
208
209    version_title_ = new views::Label(
210        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_VERSION_LABEL));
211    version_title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
212
213    version_value_ = new views::Label(version_str);
214    version_value_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
215  }
216
217  // The install date doesn't make sense for component apps.
218  if (app_->location() != extensions::Manifest::COMPONENT) {
219    installed_time_title_ = new views::Label(
220        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_INSTALLED_LABEL));
221    installed_time_title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
222
223    installed_time_value_ =
224        new views::Label(base::TimeFormatShortDate(GetInstalledTime()));
225    installed_time_value_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
226  }
227
228  // The last run time is currently incorrect for component and hosted apps,
229  // since it is not updated when they are accessed outside of their shortcuts.
230  // TODO(sashab): Update the run time for these correctly: crbug.com/398716
231  if (app_->location() != extensions::Manifest::COMPONENT &&
232      !app_->is_hosted_app()) {
233    last_run_time_title_ = new views::Label(
234        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_LAST_RUN_LABEL));
235    last_run_time_title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
236
237    // Display 'Never' if the app has never been run.
238    base::string16 last_run_value_str =
239        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_LAST_RUN_NEVER_LABEL);
240    if (GetLastLaunchedTime() != base::Time())
241      last_run_value_str = base::TimeFormatShortDate(GetLastLaunchedTime());
242
243    last_run_time_value_ = new views::Label(last_run_value_str);
244    last_run_time_value_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
245  }
246
247  // Only generate the heading if we have at least one field to display.
248  if (version_title_ || installed_time_title_ || last_run_time_title_) {
249    details_heading_ = CreateHeading(
250        l10n_util::GetStringUTF16(IDS_APPLICATION_INFO_DETAILS_TITLE));
251  }
252}
253
254void AppInfoSummaryPanel::CreateLaunchOptionControl() {
255  if (CanSetLaunchType()) {
256    launch_options_combobox_model_.reset(new LaunchOptionsComboboxModel());
257    launch_options_combobox_ =
258        new views::Combobox(launch_options_combobox_model_.get());
259
260    launch_options_combobox_->set_listener(this);
261    launch_options_combobox_->SetSelectedIndex(
262        launch_options_combobox_model_->GetIndexForLaunchType(GetLaunchType()));
263  }
264}
265
266void AppInfoSummaryPanel::LayoutDescriptionControl() {
267  if (description_label_) {
268    DCHECK(description_heading_);
269    views::View* vertical_stack = CreateVerticalStack();
270    vertical_stack->AddChildView(description_heading_);
271    vertical_stack->AddChildView(description_label_);
272    AddChildView(vertical_stack);
273  }
274}
275
276void AppInfoSummaryPanel::LayoutDetailsControl() {
277  if (details_heading_) {
278    views::View* details_stack =
279        CreateVerticalStack(views::kRelatedControlSmallVerticalSpacing);
280
281    if (version_title_ && version_value_) {
282      details_stack->AddChildView(
283          CreateKeyValueField(version_title_, version_value_));
284    }
285
286    if (installed_time_title_ && installed_time_value_) {
287      details_stack->AddChildView(
288          CreateKeyValueField(installed_time_title_, installed_time_value_));
289    }
290
291    if (last_run_time_title_ && last_run_time_value_) {
292      details_stack->AddChildView(
293          CreateKeyValueField(last_run_time_title_, last_run_time_value_));
294    }
295
296    if (size_title_ && size_value_) {
297      details_stack->AddChildView(
298          CreateKeyValueField(size_title_, size_value_));
299    }
300
301    views::View* vertical_stack = CreateVerticalStack();
302    vertical_stack->AddChildView(details_heading_);
303    vertical_stack->AddChildView(details_stack);
304    AddChildView(vertical_stack);
305  }
306}
307
308void AppInfoSummaryPanel::OnPerformAction(views::Combobox* combobox) {
309  if (combobox == launch_options_combobox_) {
310    SetLaunchType(launch_options_combobox_model_->GetLaunchTypeAtIndex(
311        launch_options_combobox_->selected_index()));
312  } else {
313    NOTREACHED();
314  }
315}
316
317void AppInfoSummaryPanel::StartCalculatingAppSize() {
318  base::PostTaskAndReplyWithResult(
319      content::BrowserThread::GetBlockingPool(),
320      FROM_HERE,
321      base::Bind(&base::ComputeDirectorySize, app_->path()),
322      base::Bind(&AppInfoSummaryPanel::OnAppSizeCalculated, AsWeakPtr()));
323}
324
325void AppInfoSummaryPanel::OnAppSizeCalculated(int64 app_size_in_bytes) {
326  size_value_->SetText(ui::FormatBytes(app_size_in_bytes));
327}
328
329base::Time AppInfoSummaryPanel::GetInstalledTime() const {
330  return extensions::ExtensionPrefs::Get(profile_)->GetInstallTime(app_->id());
331}
332
333base::Time AppInfoSummaryPanel::GetLastLaunchedTime() const {
334  return extensions::ExtensionPrefs::Get(profile_)
335      ->GetLastLaunchTime(app_->id());
336}
337
338extensions::LaunchType AppInfoSummaryPanel::GetLaunchType() const {
339  return extensions::GetLaunchType(extensions::ExtensionPrefs::Get(profile_),
340                                   app_);
341}
342
343void AppInfoSummaryPanel::SetLaunchType(
344    extensions::LaunchType launch_type) const {
345  DCHECK(CanSetLaunchType());
346  ExtensionService* service =
347      extensions::ExtensionSystem::Get(profile_)->extension_service();
348  extensions::SetLaunchType(service, app_->id(), launch_type);
349}
350
351bool AppInfoSummaryPanel::CanSetLaunchType() const {
352  // V2 apps don't have a launch type, and neither does the Chrome app.
353  return app_->id() != extension_misc::kChromeAppId && !app_->is_platform_app();
354}
355