extension_app_item.cc revision a93a17c8d99d686bd4a1511e5504e5e6cc9fcadf
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/app_list/extension_app_item.h"
6
7#include "base/prefs/pref_service.h"
8#include "chrome/app/chrome_command_ids.h"
9#include "chrome/browser/extensions/context_menu_matcher.h"
10#include "chrome/browser/extensions/extension_prefs.h"
11#include "chrome/browser/extensions/extension_service.h"
12#include "chrome/browser/extensions/extension_sorting.h"
13#include "chrome/browser/extensions/extension_system.h"
14#include "chrome/browser/extensions/extension_uninstall_dialog.h"
15#include "chrome/browser/extensions/management_policy.h"
16#include "chrome/browser/prefs/incognito_mode_prefs.h"
17#include "chrome/browser/profiles/profile.h"
18#include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
19#include "chrome/browser/ui/browser_navigator.h"
20#include "chrome/browser/ui/browser_tabstrip.h"
21#include "chrome/browser/ui/browser_window.h"
22#include "chrome/browser/ui/extensions/extension_enable_flow.h"
23#include "chrome/browser/ui/webui/ntp/app_launcher_handler.h"
24#include "chrome/common/extensions/extension.h"
25#include "chrome/common/extensions/extension_constants.h"
26#include "chrome/common/extensions/extension_icon_set.h"
27#include "chrome/common/extensions/manifest_handlers/icons_handler.h"
28#include "chrome/common/extensions/manifest_url_handler.h"
29#include "content/public/common/context_menu_params.h"
30#include "grit/chromium_strings.h"
31#include "grit/generated_resources.h"
32#include "grit/theme_resources.h"
33#include "ui/base/l10n/l10n_util.h"
34#include "ui/base/resource/resource_bundle.h"
35#include "ui/gfx/canvas.h"
36#include "ui/gfx/color_utils.h"
37#include "ui/gfx/image/canvas_image_source.h"
38#include "ui/gfx/image/image_skia_operations.h"
39
40#if defined(USE_ASH)
41#include "ash/shell.h"
42#endif
43
44using extensions::Extension;
45
46namespace {
47
48enum CommandId {
49  LAUNCH_NEW = 100,
50  TOGGLE_PIN,
51  CREATE_SHORTCUTS,
52  OPTIONS,
53  UNINSTALL,
54  DETAILS,
55  MENU_NEW_WINDOW,
56  MENU_NEW_INCOGNITO_WINDOW,
57  // Order matters in LAUNCHER_TYPE_xxxx and must match LaunchType.
58  LAUNCH_TYPE_START = 200,
59  LAUNCH_TYPE_PINNED_TAB = LAUNCH_TYPE_START,
60  LAUNCH_TYPE_REGULAR_TAB,
61  LAUNCH_TYPE_FULLSCREEN,
62  LAUNCH_TYPE_WINDOW,
63  LAUNCH_TYPE_LAST,
64};
65
66// ExtensionUninstaller decouples ExtensionAppItem from the extension uninstall
67// flow. It shows extension uninstall dialog and wait for user to confirm or
68// cancel the uninstall.
69class ExtensionUninstaller : public ExtensionUninstallDialog::Delegate {
70 public:
71  ExtensionUninstaller(Profile* profile,
72                       const std::string& extension_id,
73                       AppListControllerDelegate* controller)
74      : profile_(profile),
75        extension_id_(extension_id),
76        controller_(controller) {
77  }
78
79  void Run() {
80    const Extension* extension =
81        extensions::ExtensionSystem::Get(profile_)->extension_service()->
82            GetExtensionById(extension_id_, true);
83    if (!extension) {
84      CleanUp();
85      return;
86    }
87    controller_->OnShowExtensionPrompt();
88    dialog_.reset(ExtensionUninstallDialog::Create(profile_, NULL, this));
89    dialog_->ConfirmUninstall(extension);
90  }
91
92 private:
93  // Overridden from ExtensionUninstallDialog::Delegate:
94  virtual void ExtensionUninstallAccepted() OVERRIDE {
95    ExtensionService* service =
96        extensions::ExtensionSystem::Get(profile_)->extension_service();
97    const Extension* extension = service->GetInstalledExtension(extension_id_);
98    if (extension) {
99      service->UninstallExtension(extension_id_,
100                                  false, /* external_uninstall*/
101                                  NULL);
102    }
103    controller_->OnCloseExtensionPrompt();
104    CleanUp();
105  }
106
107  virtual void ExtensionUninstallCanceled() OVERRIDE {
108    controller_->OnCloseExtensionPrompt();
109    CleanUp();
110  }
111
112  void CleanUp() {
113    delete this;
114  }
115
116  Profile* profile_;
117  std::string extension_id_;
118  AppListControllerDelegate* controller_;
119  scoped_ptr<ExtensionUninstallDialog> dialog_;
120
121  DISALLOW_COPY_AND_ASSIGN(ExtensionUninstaller);
122};
123
124// Overlays a shortcut icon over the bottom left corner of a given image.
125class ShortcutOverlayImageSource : public gfx::CanvasImageSource {
126 public:
127  explicit ShortcutOverlayImageSource(const gfx::ImageSkia& icon)
128      : gfx::CanvasImageSource(icon.size(), false),
129        icon_(icon) {
130  }
131  virtual ~ShortcutOverlayImageSource() {}
132
133 private:
134  // gfx::CanvasImageSource overrides:
135  virtual void Draw(gfx::Canvas* canvas) OVERRIDE {
136    canvas->DrawImageInt(icon_, 0, 0);
137
138    // Draw the overlay in the bottom left corner of the icon.
139    const gfx::ImageSkia& overlay = *ui::ResourceBundle::GetSharedInstance().
140        GetImageSkiaNamed(IDR_APP_LIST_TAB_OVERLAY);
141    canvas->DrawImageInt(overlay, 0, icon_.height() - overlay.height());
142  }
143
144  gfx::ImageSkia icon_;
145
146  DISALLOW_COPY_AND_ASSIGN(ShortcutOverlayImageSource);
147};
148
149extensions::ExtensionPrefs::LaunchType GetExtensionLaunchType(
150    Profile* profile,
151    const Extension* extension) {
152  ExtensionService* service =
153      extensions::ExtensionSystem::Get(profile)->extension_service();
154  return service->extension_prefs()->
155      GetLaunchType(extension, extensions::ExtensionPrefs::LAUNCH_DEFAULT);
156}
157
158void SetExtensionLaunchType(
159    Profile* profile,
160    const std::string& extension_id,
161    extensions::ExtensionPrefs::LaunchType launch_type) {
162  ExtensionService* service =
163      extensions::ExtensionSystem::Get(profile)->extension_service();
164  service->extension_prefs()->SetLaunchType(extension_id, launch_type);
165}
166
167ExtensionSorting* GetExtensionSorting(Profile* profile) {
168  ExtensionService* service =
169      extensions::ExtensionSystem::Get(profile)->extension_service();
170  return service->extension_prefs()->extension_sorting();
171}
172
173bool MenuItemHasLauncherContext(const extensions::MenuItem* item) {
174  return item->contexts().Contains(extensions::MenuItem::LAUNCHER);
175}
176
177const color_utils::HSL shift = {-1, 0, 0.6};
178
179}  // namespace
180
181ExtensionAppItem::ExtensionAppItem(Profile* profile,
182                                   const std::string& extension_id,
183                                   AppListControllerDelegate* controller,
184                                   const std::string& extension_name,
185                                   const gfx::ImageSkia& installing_icon,
186                                   bool is_platform_app)
187    : ChromeAppListItem(TYPE_APP),
188      profile_(profile),
189      extension_id_(extension_id),
190      controller_(controller),
191      extension_name_(extension_name),
192      installing_icon_(
193          gfx::ImageSkiaOperations::CreateHSLShiftedImage(installing_icon,
194                                                          shift)),
195      is_platform_app_(is_platform_app) {
196  Reload();
197  GetExtensionSorting(profile_)->EnsureValidOrdinals(extension_id_,
198                                                     syncer::StringOrdinal());
199}
200
201ExtensionAppItem::~ExtensionAppItem() {
202}
203
204bool ExtensionAppItem::HasOverlay() const {
205#if defined(OS_CHROMEOS)
206  return false;
207#else
208  return !is_platform_app_ && extension_id_ != extension_misc::kChromeAppId;
209#endif
210}
211
212void ExtensionAppItem::Reload() {
213  const Extension* extension = GetExtension();
214  bool is_installing = !extension;
215  SetIsInstalling(is_installing);
216  set_app_id(extension_id_);
217  if (is_installing) {
218    SetTitle(extension_name_);
219    UpdateIcon();
220    return;
221  }
222  SetTitle(extension->name());
223  LoadImage(extension);
224}
225
226syncer::StringOrdinal ExtensionAppItem::GetPageOrdinal() const {
227  return GetExtensionSorting(profile_)->GetPageOrdinal(extension_id_);
228}
229
230syncer::StringOrdinal ExtensionAppItem::GetAppLaunchOrdinal() const {
231  return GetExtensionSorting(profile_)->GetAppLaunchOrdinal(extension_id_);
232}
233
234void ExtensionAppItem::Move(const ExtensionAppItem* prev,
235                            const ExtensionAppItem* next) {
236  // Does nothing if no predecessor nor successor.
237  if (!prev && !next)
238    return;
239
240  ExtensionService* service =
241      extensions::ExtensionSystem::Get(profile_)->extension_service();
242  service->extension_prefs()->SetAppDraggedByUser(extension_id_);
243
244  // Handles only predecessor or only successor case.
245  if (!prev || !next) {
246    syncer::StringOrdinal page = prev ? prev->GetPageOrdinal() :
247                                        next->GetPageOrdinal();
248    GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, page);
249    service->OnExtensionMoved(extension_id_,
250                              prev ? prev->extension_id() : std::string(),
251                              next ? next->extension_id() : std::string());
252    return;
253  }
254
255  // Handles both predecessor and successor are on the same page.
256  syncer::StringOrdinal prev_page = prev->GetPageOrdinal();
257  syncer::StringOrdinal next_page = next->GetPageOrdinal();
258  if (prev_page.Equals(next_page)) {
259    GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page);
260    service->OnExtensionMoved(extension_id_,
261                              prev->extension_id(),
262                              next->extension_id());
263    return;
264  }
265
266  // Otherwise, go with |next|. This is okay because app list does not split
267  // page based ntp page ordinal.
268  // TODO(xiyuan): Revisit this when implementing paging support.
269  GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page);
270  service->OnExtensionMoved(extension_id_,
271                            prev->extension_id(),
272                            std::string());
273}
274
275void ExtensionAppItem::UpdateIcon() {
276  if (!GetExtension()) {
277    gfx::ImageSkia icon = installing_icon_;
278    if (HasOverlay())
279      icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size());
280    SetIcon(icon, !HasOverlay());
281    return;
282  }
283  gfx::ImageSkia icon = icon_->image_skia();
284
285  const ExtensionService* service =
286      extensions::ExtensionSystem::Get(profile_)->extension_service();
287  const bool enabled = service->IsExtensionEnabledForLauncher(extension_id_);
288  if (!enabled) {
289    const color_utils::HSL shift = {-1, 0, 0.6};
290    icon = gfx::ImageSkiaOperations::CreateHSLShiftedImage(icon, shift);
291  }
292
293  if (HasOverlay())
294    icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size());
295
296  SetIcon(icon, !HasOverlay());
297}
298
299const Extension* ExtensionAppItem::GetExtension() const {
300  const ExtensionService* service =
301      extensions::ExtensionSystem::Get(profile_)->extension_service();
302  const Extension* extension = service->GetInstalledExtension(extension_id_);
303  return extension;
304}
305
306void ExtensionAppItem::LoadImage(const Extension* extension) {
307  icon_.reset(new extensions::IconImage(
308      profile_,
309      extension,
310      extensions::IconsInfo::GetIcons(extension),
311      extension_misc::EXTENSION_ICON_MEDIUM,
312      extensions::IconsInfo::GetDefaultAppIcon(),
313      this));
314  UpdateIcon();
315}
316
317void ExtensionAppItem::ShowExtensionOptions() {
318  const Extension* extension = GetExtension();
319  if (!extension)
320    return;
321
322  chrome::NavigateParams params(
323      profile_,
324      extensions::ManifestURL::GetOptionsPage(extension),
325      content::PAGE_TRANSITION_LINK);
326  chrome::Navigate(&params);
327}
328
329void ExtensionAppItem::ShowExtensionDetails() {
330  const Extension* extension = GetExtension();
331  if (!extension)
332    return;
333
334  chrome::NavigateParams params(
335      profile_,
336      extensions::ManifestURL::GetDetailsURL(extension),
337      content::PAGE_TRANSITION_LINK);
338  chrome::Navigate(&params);
339}
340
341void ExtensionAppItem::StartExtensionUninstall() {
342  // ExtensionUninstall deletes itself when done or aborted.
343  ExtensionUninstaller* uninstaller = new ExtensionUninstaller(profile_,
344                                                               extension_id_,
345                                                               controller_);
346  uninstaller->Run();
347}
348
349bool ExtensionAppItem::RunExtensionEnableFlow() {
350  const ExtensionService* service =
351      extensions::ExtensionSystem::Get(profile_)->extension_service();
352  if (service->IsExtensionEnabledForLauncher(extension_id_))
353    return false;
354
355  if (!extension_enable_flow_) {
356    controller_->OnShowExtensionPrompt();
357
358    extension_enable_flow_.reset(new ExtensionEnableFlow(
359        profile_, extension_id_, this));
360    extension_enable_flow_->StartForNativeWindow(
361        controller_->GetAppListWindow());
362  }
363  return true;
364}
365
366void ExtensionAppItem::Launch(int event_flags) {
367  // |extension| could be NULL when it is being unloaded for updating.
368  const Extension* extension = GetExtension();
369  if (!extension)
370    return;
371
372  if (RunExtensionEnableFlow())
373    return;
374
375  controller_->LaunchApp(profile_, extension, event_flags);
376}
377
378void ExtensionAppItem::OnExtensionIconImageChanged(
379    extensions::IconImage* image) {
380  DCHECK(icon_.get() == image);
381  UpdateIcon();
382}
383
384void ExtensionAppItem::ExtensionEnableFlowFinished() {
385  extension_enable_flow_.reset();
386  controller_->OnCloseExtensionPrompt();
387
388  // Automatically launch app after enabling.
389  Launch(ui::EF_NONE);
390}
391
392void ExtensionAppItem::ExtensionEnableFlowAborted(bool user_initiated) {
393  extension_enable_flow_.reset();
394  controller_->OnCloseExtensionPrompt();
395}
396
397bool ExtensionAppItem::IsItemForCommandIdDynamic(int command_id) const {
398  return command_id == TOGGLE_PIN || command_id == LAUNCH_NEW;
399}
400
401string16 ExtensionAppItem::GetLabelForCommandId(int command_id) const {
402  if (command_id == TOGGLE_PIN) {
403    return controller_->IsAppPinned(extension_id_) ?
404        l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_UNPIN) :
405        l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_PIN);
406  } else if (command_id == LAUNCH_NEW) {
407    if (IsCommandIdChecked(LAUNCH_TYPE_PINNED_TAB) ||
408        IsCommandIdChecked(LAUNCH_TYPE_REGULAR_TAB)) {
409      return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_TAB);
410    } else {
411      return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW);
412    }
413  } else {
414    NOTREACHED();
415    return string16();
416  }
417}
418
419bool ExtensionAppItem::IsCommandIdChecked(int command_id) const {
420  if (command_id >= LAUNCH_TYPE_START && command_id < LAUNCH_TYPE_LAST) {
421    return static_cast<int>(GetExtensionLaunchType(profile_, GetExtension())) +
422        LAUNCH_TYPE_START == command_id;
423  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
424             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
425    return extension_menu_items_->IsCommandIdChecked(command_id);
426  }
427  return false;
428}
429
430bool ExtensionAppItem::IsCommandIdEnabled(int command_id) const {
431  if (command_id == TOGGLE_PIN) {
432    return controller_->CanPin();
433  } else if (command_id == OPTIONS) {
434    const ExtensionService* service =
435        extensions::ExtensionSystem::Get(profile_)->extension_service();
436    const Extension* extension = GetExtension();
437    return service->IsExtensionEnabledForLauncher(extension_id_) &&
438           extension &&
439           !extensions::ManifestURL::GetOptionsPage(extension).is_empty();
440  } else if (command_id == UNINSTALL) {
441    const Extension* extension = GetExtension();
442    const extensions::ManagementPolicy* policy =
443        extensions::ExtensionSystem::Get(profile_)->management_policy();
444    return extension &&
445           policy->UserMayModifySettings(extension, NULL);
446  } else if (command_id == DETAILS) {
447    const Extension* extension = GetExtension();
448    return extension && extension->from_webstore();
449  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
450             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
451    return extension_menu_items_->IsCommandIdEnabled(command_id);
452  } else if (command_id == MENU_NEW_WINDOW) {
453    // "Normal" windows are not allowed when incognito is enforced.
454    return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) !=
455        IncognitoModePrefs::FORCED;
456  } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) {
457    // Incognito windows are not allowed when incognito is disabled.
458    return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) !=
459        IncognitoModePrefs::DISABLED;
460  }
461  return true;
462}
463
464bool ExtensionAppItem::GetAcceleratorForCommandId(
465    int command_id,
466    ui::Accelerator* acclelrator) {
467  return false;
468}
469
470void ExtensionAppItem::ExecuteCommand(int command_id, int event_flags) {
471  if (command_id == LAUNCH_NEW) {
472    Launch(ui::EF_NONE);
473  } else if (command_id == TOGGLE_PIN && controller_->CanPin()) {
474    if (controller_->IsAppPinned(extension_id_))
475      controller_->UnpinApp(extension_id_);
476    else
477      controller_->PinApp(extension_id_);
478  } else if (command_id == CREATE_SHORTCUTS) {
479    controller_->ShowCreateShortcutsDialog(profile_, extension_id_);
480  } else if (command_id >= LAUNCH_TYPE_START &&
481             command_id < LAUNCH_TYPE_LAST) {
482    SetExtensionLaunchType(profile_,
483                           extension_id_,
484                           static_cast<extensions::ExtensionPrefs::LaunchType>(
485                               command_id - LAUNCH_TYPE_START));
486  } else if (command_id == OPTIONS) {
487    ShowExtensionOptions();
488  } else if (command_id == UNINSTALL) {
489    StartExtensionUninstall();
490  } else if (command_id == DETAILS) {
491    ShowExtensionDetails();
492  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
493             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
494    extension_menu_items_->ExecuteCommand(command_id, NULL,
495                                          content::ContextMenuParams());
496  } else if (command_id == MENU_NEW_WINDOW) {
497    controller_->CreateNewWindow(profile_, false);
498  } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) {
499    controller_->CreateNewWindow(profile_, true);
500  }
501}
502
503void ExtensionAppItem::Activate(int event_flags) {
504  // |extension| could be NULL when it is being unloaded for updating.
505  const Extension* extension = GetExtension();
506  if (!extension)
507    return;
508
509  if (RunExtensionEnableFlow())
510    return;
511
512  AppLauncherHandler::RecordAppLaunchType(
513      extension_misc::APP_LAUNCH_APP_LIST_MAIN,
514      extension->GetType());
515  controller_->ActivateApp(profile_, extension, event_flags);
516}
517
518ui::MenuModel* ExtensionAppItem::GetContextMenuModel() {
519  const Extension* extension = GetExtension();
520  if (!extension)
521    return NULL;
522
523  if (context_menu_model_.get())
524    return context_menu_model_.get();
525
526  context_menu_model_.reset(new ui::SimpleMenuModel(this));
527
528  if (extension_id_ == extension_misc::kChromeAppId) {
529    context_menu_model_->AddItemWithStringId(
530        MENU_NEW_WINDOW,
531        IDS_APP_LIST_NEW_WINDOW);
532    if (!profile_->IsOffTheRecord()) {
533      context_menu_model_->AddItemWithStringId(
534          MENU_NEW_INCOGNITO_WINDOW,
535          IDS_APP_LIST_NEW_INCOGNITO_WINDOW);
536    }
537  } else {
538    extension_menu_items_.reset(new extensions::ContextMenuMatcher(
539        profile_, this, context_menu_model_.get(),
540        base::Bind(MenuItemHasLauncherContext)));
541
542    if (!is_platform_app_)
543      context_menu_model_->AddItem(LAUNCH_NEW, string16());
544
545    int index = 0;
546    extension_menu_items_->AppendExtensionItems(extension_id_, string16(),
547                                                &index);
548
549    if (controller_->CanPin()) {
550      context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
551      context_menu_model_->AddItemWithStringId(
552          TOGGLE_PIN,
553          controller_->IsAppPinned(extension_id_) ?
554              IDS_APP_LIST_CONTEXT_MENU_UNPIN :
555              IDS_APP_LIST_CONTEXT_MENU_PIN);
556    }
557
558    if (controller_->CanShowCreateShortcutsDialog()) {
559      context_menu_model_->AddItemWithStringId(CREATE_SHORTCUTS,
560                                               IDS_NEW_TAB_APP_CREATE_SHORTCUT);
561    }
562
563    if (!is_platform_app_) {
564      context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
565      context_menu_model_->AddCheckItemWithStringId(
566          LAUNCH_TYPE_REGULAR_TAB,
567          IDS_APP_CONTEXT_MENU_OPEN_REGULAR);
568      context_menu_model_->AddCheckItemWithStringId(
569          LAUNCH_TYPE_PINNED_TAB,
570          IDS_APP_CONTEXT_MENU_OPEN_PINNED);
571#if defined(USE_ASH)
572      if (!ash::Shell::IsForcedMaximizeMode())
573#endif
574      {
575        context_menu_model_->AddCheckItemWithStringId(
576            LAUNCH_TYPE_WINDOW,
577            IDS_APP_CONTEXT_MENU_OPEN_WINDOW);
578        // Even though the launch type is Full Screen it is more accurately
579        // described as Maximized in Ash.
580        context_menu_model_->AddCheckItemWithStringId(
581            LAUNCH_TYPE_FULLSCREEN,
582            IDS_APP_CONTEXT_MENU_OPEN_MAXIMIZED);
583      }
584      context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
585      context_menu_model_->AddItemWithStringId(OPTIONS,
586                                               IDS_NEW_TAB_APP_OPTIONS);
587    }
588
589    context_menu_model_->AddItemWithStringId(DETAILS,
590                                             IDS_NEW_TAB_APP_DETAILS);
591    context_menu_model_->AddItemWithStringId(UNINSTALL,
592                                             is_platform_app_ ?
593                                                 IDS_APP_LIST_UNINSTALL_ITEM :
594                                                 IDS_EXTENSIONS_UNINSTALL);
595  }
596
597  return context_menu_model_.get();
598}
599