extension_app_item.cc revision b2df76ea8fec9e32f6f3718986dba0d95315b29c
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  return !is_platform_app_ && extension_id_ != extension_misc::kChromeAppId;
206}
207
208void ExtensionAppItem::Reload() {
209  const Extension* extension = GetExtension();
210  bool is_installing = !extension;
211  SetIsInstalling(is_installing);
212  set_app_id(extension_id_);
213  if (is_installing) {
214    SetTitle(extension_name_);
215    UpdateIcon();
216    return;
217  }
218  SetTitle(extension->name());
219  LoadImage(extension);
220}
221
222syncer::StringOrdinal ExtensionAppItem::GetPageOrdinal() const {
223  return GetExtensionSorting(profile_)->GetPageOrdinal(extension_id_);
224}
225
226syncer::StringOrdinal ExtensionAppItem::GetAppLaunchOrdinal() const {
227  return GetExtensionSorting(profile_)->GetAppLaunchOrdinal(extension_id_);
228}
229
230void ExtensionAppItem::Move(const ExtensionAppItem* prev,
231                            const ExtensionAppItem* next) {
232  // Does nothing if no predecessor nor successor.
233  if (!prev && !next)
234    return;
235
236  ExtensionService* service =
237      extensions::ExtensionSystem::Get(profile_)->extension_service();
238  service->extension_prefs()->SetAppDraggedByUser(extension_id_);
239
240  // Handles only predecessor or only successor case.
241  if (!prev || !next) {
242    syncer::StringOrdinal page = prev ? prev->GetPageOrdinal() :
243                                        next->GetPageOrdinal();
244    GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, page);
245    service->OnExtensionMoved(extension_id_,
246                              prev ? prev->extension_id() : std::string(),
247                              next ? next->extension_id() : std::string());
248    return;
249  }
250
251  // Handles both predecessor and successor are on the same page.
252  syncer::StringOrdinal prev_page = prev->GetPageOrdinal();
253  syncer::StringOrdinal next_page = next->GetPageOrdinal();
254  if (prev_page.Equals(next_page)) {
255    GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page);
256    service->OnExtensionMoved(extension_id_,
257                              prev->extension_id(),
258                              next->extension_id());
259    return;
260  }
261
262  // Otherwise, go with |next|. This is okay because app list does not split
263  // page based ntp page ordinal.
264  // TODO(xiyuan): Revisit this when implementing paging support.
265  GetExtensionSorting(profile_)->SetPageOrdinal(extension_id_, prev_page);
266  service->OnExtensionMoved(extension_id_,
267                            prev->extension_id(),
268                            std::string());
269}
270
271void ExtensionAppItem::UpdateIcon() {
272  if (!GetExtension()) {
273    gfx::ImageSkia icon = installing_icon_;
274    if (HasOverlay())
275      icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size());
276    SetIcon(icon, !HasOverlay());
277    return;
278  }
279  gfx::ImageSkia icon = icon_->image_skia();
280
281  const ExtensionService* service =
282      extensions::ExtensionSystem::Get(profile_)->extension_service();
283  const bool enabled = service->IsExtensionEnabledForLauncher(extension_id_);
284  if (!enabled) {
285    const color_utils::HSL shift = {-1, 0, 0.6};
286    icon = gfx::ImageSkiaOperations::CreateHSLShiftedImage(icon, shift);
287  }
288
289  if (HasOverlay())
290    icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size());
291
292  SetIcon(icon, !HasOverlay());
293}
294
295const Extension* ExtensionAppItem::GetExtension() const {
296  const ExtensionService* service =
297      extensions::ExtensionSystem::Get(profile_)->extension_service();
298  const Extension* extension = service->GetInstalledExtension(extension_id_);
299  return extension;
300}
301
302void ExtensionAppItem::LoadImage(const Extension* extension) {
303  icon_.reset(new extensions::IconImage(
304      profile_,
305      extension,
306      extensions::IconsInfo::GetIcons(extension),
307      extension_misc::EXTENSION_ICON_MEDIUM,
308      extensions::IconsInfo::GetDefaultAppIcon(),
309      this));
310  UpdateIcon();
311}
312
313void ExtensionAppItem::ShowExtensionOptions() {
314  const Extension* extension = GetExtension();
315  if (!extension)
316    return;
317
318  chrome::NavigateParams params(
319      profile_,
320      extensions::ManifestURL::GetOptionsPage(extension),
321      content::PAGE_TRANSITION_LINK);
322  chrome::Navigate(&params);
323}
324
325void ExtensionAppItem::ShowExtensionDetails() {
326  const Extension* extension = GetExtension();
327  if (!extension)
328    return;
329
330  chrome::NavigateParams params(
331      profile_,
332      extensions::ManifestURL::GetDetailsURL(extension),
333      content::PAGE_TRANSITION_LINK);
334  chrome::Navigate(&params);
335}
336
337void ExtensionAppItem::StartExtensionUninstall() {
338  // ExtensionUninstall deletes itself when done or aborted.
339  ExtensionUninstaller* uninstaller = new ExtensionUninstaller(profile_,
340                                                               extension_id_,
341                                                               controller_);
342  uninstaller->Run();
343}
344
345bool ExtensionAppItem::RunExtensionEnableFlow() {
346  const ExtensionService* service =
347      extensions::ExtensionSystem::Get(profile_)->extension_service();
348  if (service->IsExtensionEnabledForLauncher(extension_id_))
349    return false;
350
351  if (!extension_enable_flow_) {
352    controller_->OnShowExtensionPrompt();
353
354    extension_enable_flow_.reset(new ExtensionEnableFlow(
355        profile_, extension_id_, this));
356    extension_enable_flow_->StartForNativeWindow(
357        controller_->GetAppListWindow());
358  }
359  return true;
360}
361
362void ExtensionAppItem::Launch(int event_flags) {
363  // |extension| could be NULL when it is being unloaded for updating.
364  const Extension* extension = GetExtension();
365  if (!extension)
366    return;
367
368  if (RunExtensionEnableFlow())
369    return;
370
371  controller_->LaunchApp(profile_, extension, event_flags);
372}
373
374void ExtensionAppItem::OnExtensionIconImageChanged(
375    extensions::IconImage* image) {
376  DCHECK(icon_.get() == image);
377  UpdateIcon();
378}
379
380void ExtensionAppItem::ExtensionEnableFlowFinished() {
381  extension_enable_flow_.reset();
382  controller_->OnCloseExtensionPrompt();
383
384  // Automatically launch app after enabling.
385  Launch(ui::EF_NONE);
386}
387
388void ExtensionAppItem::ExtensionEnableFlowAborted(bool user_initiated) {
389  extension_enable_flow_.reset();
390  controller_->OnCloseExtensionPrompt();
391}
392
393bool ExtensionAppItem::IsItemForCommandIdDynamic(int command_id) const {
394  return command_id == TOGGLE_PIN || command_id == LAUNCH_NEW;
395}
396
397string16 ExtensionAppItem::GetLabelForCommandId(int command_id) const {
398  if (command_id == TOGGLE_PIN) {
399    return controller_->IsAppPinned(extension_id_) ?
400        l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_UNPIN) :
401        l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_PIN);
402  } else if (command_id == LAUNCH_NEW) {
403    if (IsCommandIdChecked(LAUNCH_TYPE_PINNED_TAB) ||
404        IsCommandIdChecked(LAUNCH_TYPE_REGULAR_TAB)) {
405      return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_TAB);
406    } else {
407      return l10n_util::GetStringUTF16(IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW);
408    }
409  } else {
410    NOTREACHED();
411    return string16();
412  }
413}
414
415bool ExtensionAppItem::IsCommandIdChecked(int command_id) const {
416  if (command_id >= LAUNCH_TYPE_START && command_id < LAUNCH_TYPE_LAST) {
417    return static_cast<int>(GetExtensionLaunchType(profile_, GetExtension())) +
418        LAUNCH_TYPE_START == command_id;
419  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
420             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
421    return extension_menu_items_->IsCommandIdChecked(command_id);
422  }
423  return false;
424}
425
426bool ExtensionAppItem::IsCommandIdEnabled(int command_id) const {
427  if (command_id == TOGGLE_PIN) {
428    return controller_->CanPin();
429  } else if (command_id == OPTIONS) {
430    const ExtensionService* service =
431        extensions::ExtensionSystem::Get(profile_)->extension_service();
432    const Extension* extension = GetExtension();
433    return service->IsExtensionEnabledForLauncher(extension_id_) &&
434           extension &&
435           !extensions::ManifestURL::GetOptionsPage(extension).is_empty();
436  } else if (command_id == UNINSTALL) {
437    const Extension* extension = GetExtension();
438    const extensions::ManagementPolicy* policy =
439        extensions::ExtensionSystem::Get(profile_)->management_policy();
440    return extension &&
441           policy->UserMayModifySettings(extension, NULL);
442  } else if (command_id == DETAILS) {
443    const Extension* extension = GetExtension();
444    return extension && extension->from_webstore();
445  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
446             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
447    return extension_menu_items_->IsCommandIdEnabled(command_id);
448  } else if (command_id == MENU_NEW_WINDOW) {
449    // "Normal" windows are not allowed when incognito is enforced.
450    return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) !=
451        IncognitoModePrefs::FORCED;
452  } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) {
453    // Incognito windows are not allowed when incognito is disabled.
454    return IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) !=
455        IncognitoModePrefs::DISABLED;
456  }
457  return true;
458}
459
460bool ExtensionAppItem::GetAcceleratorForCommandId(
461    int command_id,
462    ui::Accelerator* acclelrator) {
463  return false;
464}
465
466void ExtensionAppItem::ExecuteCommand(int command_id, int event_flags) {
467  if (command_id == LAUNCH_NEW) {
468    Launch(ui::EF_NONE);
469  } else if (command_id == TOGGLE_PIN && controller_->CanPin()) {
470    if (controller_->IsAppPinned(extension_id_))
471      controller_->UnpinApp(extension_id_);
472    else
473      controller_->PinApp(extension_id_);
474  } else if (command_id == CREATE_SHORTCUTS) {
475    controller_->ShowCreateShortcutsDialog(profile_, extension_id_);
476  } else if (command_id >= LAUNCH_TYPE_START &&
477             command_id < LAUNCH_TYPE_LAST) {
478    SetExtensionLaunchType(profile_,
479                           extension_id_,
480                           static_cast<extensions::ExtensionPrefs::LaunchType>(
481                               command_id - LAUNCH_TYPE_START));
482  } else if (command_id == OPTIONS) {
483    ShowExtensionOptions();
484  } else if (command_id == UNINSTALL) {
485    StartExtensionUninstall();
486  } else if (command_id == DETAILS) {
487    ShowExtensionDetails();
488  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
489             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
490    extension_menu_items_->ExecuteCommand(command_id, NULL,
491                                          content::ContextMenuParams());
492  } else if (command_id == MENU_NEW_WINDOW) {
493    controller_->CreateNewWindow(profile_, false);
494  } else if (command_id == MENU_NEW_INCOGNITO_WINDOW) {
495    controller_->CreateNewWindow(profile_, true);
496  }
497}
498
499void ExtensionAppItem::Activate(int event_flags) {
500  // |extension| could be NULL when it is being unloaded for updating.
501  const Extension* extension = GetExtension();
502  if (!extension)
503    return;
504
505  if (RunExtensionEnableFlow())
506    return;
507
508  AppLauncherHandler::RecordAppLaunchType(
509      extension_misc::APP_LAUNCH_APP_LIST_MAIN,
510      extension->GetType());
511  controller_->ActivateApp(profile_, extension, event_flags);
512}
513
514ui::MenuModel* ExtensionAppItem::GetContextMenuModel() {
515  const Extension* extension = GetExtension();
516  if (!extension)
517    return NULL;
518
519  if (context_menu_model_.get())
520    return context_menu_model_.get();
521
522  context_menu_model_.reset(new ui::SimpleMenuModel(this));
523
524  if (extension_id_ == extension_misc::kChromeAppId) {
525    context_menu_model_->AddItemWithStringId(
526        MENU_NEW_WINDOW,
527        IDS_APP_LIST_NEW_WINDOW);
528    if (!profile_->IsOffTheRecord()) {
529      context_menu_model_->AddItemWithStringId(
530          MENU_NEW_INCOGNITO_WINDOW,
531          IDS_APP_LIST_NEW_INCOGNITO_WINDOW);
532    }
533  } else {
534    extension_menu_items_.reset(new extensions::ContextMenuMatcher(
535        profile_, this, context_menu_model_.get(),
536        base::Bind(MenuItemHasLauncherContext)));
537
538    if (!is_platform_app_)
539      context_menu_model_->AddItem(LAUNCH_NEW, string16());
540
541    int index = 0;
542    extension_menu_items_->AppendExtensionItems(extension_id_, string16(),
543                                                &index);
544
545    if (controller_->CanPin()) {
546      context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
547      context_menu_model_->AddItemWithStringId(
548          TOGGLE_PIN,
549          controller_->IsAppPinned(extension_id_) ?
550              IDS_APP_LIST_CONTEXT_MENU_UNPIN :
551              IDS_APP_LIST_CONTEXT_MENU_PIN);
552    }
553
554    if (controller_->CanShowCreateShortcutsDialog()) {
555      context_menu_model_->AddItemWithStringId(CREATE_SHORTCUTS,
556                                               IDS_NEW_TAB_APP_CREATE_SHORTCUT);
557    }
558
559    if (!is_platform_app_) {
560      context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
561      context_menu_model_->AddCheckItemWithStringId(
562          LAUNCH_TYPE_REGULAR_TAB,
563          IDS_APP_CONTEXT_MENU_OPEN_REGULAR);
564      context_menu_model_->AddCheckItemWithStringId(
565          LAUNCH_TYPE_PINNED_TAB,
566          IDS_APP_CONTEXT_MENU_OPEN_PINNED);
567#if defined(USE_ASH)
568      if (!ash::Shell::IsForcedMaximizeMode())
569#endif
570      {
571        context_menu_model_->AddCheckItemWithStringId(
572            LAUNCH_TYPE_WINDOW,
573            IDS_APP_CONTEXT_MENU_OPEN_WINDOW);
574        // Even though the launch type is Full Screen it is more accurately
575        // described as Maximized in Ash.
576        context_menu_model_->AddCheckItemWithStringId(
577            LAUNCH_TYPE_FULLSCREEN,
578            IDS_APP_CONTEXT_MENU_OPEN_MAXIMIZED);
579      }
580      context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
581      context_menu_model_->AddItemWithStringId(OPTIONS,
582                                               IDS_NEW_TAB_APP_OPTIONS);
583    }
584
585    context_menu_model_->AddItemWithStringId(DETAILS,
586                                             IDS_NEW_TAB_APP_DETAILS);
587    context_menu_model_->AddItemWithStringId(UNINSTALL,
588                                             is_platform_app_ ?
589                                                 IDS_APP_LIST_UNINSTALL_ITEM :
590                                                 IDS_EXTENSIONS_UNINSTALL);
591  }
592
593  return context_menu_model_.get();
594}
595