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/browser/extensions/extension_service.h"
9#include "chrome/browser/extensions/extension_util.h"
10#include "chrome/browser/extensions/launch_util.h"
11#include "chrome/browser/profiles/profile.h"
12#include "chrome/browser/ui/app_list/app_context_menu.h"
13#include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
14#include "chrome/browser/ui/app_list/app_list_service.h"
15#include "chrome/browser/ui/extensions/extension_enable_flow.h"
16#include "chrome/browser/ui/host_desktop.h"
17#include "chrome/browser/ui/webui/ntp/core_app_launcher_handler.h"
18#include "chrome/common/extensions/extension_constants.h"
19#include "chrome/common/extensions/manifest_url_handler.h"
20#include "content/public/browser/user_metrics.h"
21#include "extensions/browser/app_sorting.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/extension_icon_set.h"
26#include "extensions/common/manifest_handlers/icons_handler.h"
27#include "grit/theme_resources.h"
28#include "sync/api/string_ordinal.h"
29#include "ui/base/resource/resource_bundle.h"
30#include "ui/gfx/canvas.h"
31#include "ui/gfx/color_utils.h"
32#include "ui/gfx/image/canvas_image_source.h"
33#include "ui/gfx/image/image_skia_operations.h"
34#include "ui/gfx/rect.h"
35
36using extensions::Extension;
37
38namespace {
39
40// Overlays a shortcut icon over the bottom left corner of a given image.
41class ShortcutOverlayImageSource : public gfx::CanvasImageSource {
42 public:
43  explicit ShortcutOverlayImageSource(const gfx::ImageSkia& icon)
44      : gfx::CanvasImageSource(icon.size(), false),
45        icon_(icon) {
46  }
47  virtual ~ShortcutOverlayImageSource() {}
48
49 private:
50  // gfx::CanvasImageSource overrides:
51  virtual void Draw(gfx::Canvas* canvas) OVERRIDE {
52    canvas->DrawImageInt(icon_, 0, 0);
53
54    // Draw the overlay in the bottom left corner of the icon.
55    const gfx::ImageSkia& overlay = *ui::ResourceBundle::GetSharedInstance().
56        GetImageSkiaNamed(IDR_APP_LIST_TAB_OVERLAY);
57    canvas->DrawImageInt(overlay, 0, icon_.height() - overlay.height());
58  }
59
60  gfx::ImageSkia icon_;
61
62  DISALLOW_COPY_AND_ASSIGN(ShortcutOverlayImageSource);
63};
64
65// Rounds the corners of a given image.
66class RoundedCornersImageSource : public gfx::CanvasImageSource {
67 public:
68  explicit RoundedCornersImageSource(const gfx::ImageSkia& icon)
69      : gfx::CanvasImageSource(icon.size(), false),
70        icon_(icon) {
71  }
72  virtual ~RoundedCornersImageSource() {}
73
74 private:
75  // gfx::CanvasImageSource overrides:
76  virtual void Draw(gfx::Canvas* canvas) OVERRIDE {
77    // The radius used to round the app icon.
78    const size_t kRoundingRadius = 2;
79
80    canvas->DrawImageInt(icon_, 0, 0);
81
82    scoped_ptr<gfx::Canvas> masking_canvas(
83        new gfx::Canvas(gfx::Size(icon_.width(), icon_.height()), 1.0f, false));
84    DCHECK(masking_canvas);
85
86    SkPaint opaque_paint;
87    opaque_paint.setColor(SK_ColorWHITE);
88    opaque_paint.setFlags(SkPaint::kAntiAlias_Flag);
89    masking_canvas->DrawRoundRect(
90        gfx::Rect(icon_.width(), icon_.height()),
91        kRoundingRadius, opaque_paint);
92
93    SkPaint masking_paint;
94    masking_paint.setXfermodeMode(SkXfermode::kDstIn_Mode);
95    canvas->DrawImageInt(
96        gfx::ImageSkia(masking_canvas->ExtractImageRep()), 0, 0, masking_paint);
97  }
98
99  gfx::ImageSkia icon_;
100
101  DISALLOW_COPY_AND_ASSIGN(RoundedCornersImageSource);
102};
103
104extensions::AppSorting* GetAppSorting(Profile* profile) {
105  return extensions::ExtensionPrefs::Get(profile)->app_sorting();
106}
107
108const color_utils::HSL shift = {-1, 0, 0.6};
109
110}  // namespace
111
112ExtensionAppItem::ExtensionAppItem(
113    Profile* profile,
114    const app_list::AppListSyncableService::SyncItem* sync_item,
115    const std::string& extension_id,
116    const std::string& extension_name,
117    const gfx::ImageSkia& installing_icon,
118    bool is_platform_app)
119    : app_list::AppListItem(extension_id),
120      profile_(profile),
121      extension_id_(extension_id),
122      extension_enable_flow_controller_(NULL),
123      extension_name_(extension_name),
124      installing_icon_(
125          gfx::ImageSkiaOperations::CreateHSLShiftedImage(installing_icon,
126                                                          shift)),
127      is_platform_app_(is_platform_app),
128      has_overlay_(false) {
129  Reload();
130  if (sync_item && sync_item->item_ordinal.IsValid()) {
131    // An existing synced position exists, use that.
132    set_position(sync_item->item_ordinal);
133    // Only set the name from the sync item if it is empty.
134    if (name().empty())
135      SetName(sync_item->item_name);
136    return;
137  }
138  GetAppSorting(profile_)->EnsureValidOrdinals(extension_id_,
139                                               syncer::StringOrdinal());
140  UpdatePositionFromExtensionOrdering();
141}
142
143ExtensionAppItem::~ExtensionAppItem() {
144}
145
146bool ExtensionAppItem::NeedsOverlay() const {
147  // The overlay icon is disabled for hosted apps in windowed mode with
148  // streamlined hosted apps.
149  bool streamlined_hosted_apps =
150      extensions::util::IsStreamlinedHostedAppsEnabled();
151#if defined(OS_CHROMEOS)
152  if (!streamlined_hosted_apps)
153    return false;
154#endif
155  extensions::LaunchType launch_type =
156      GetExtension()
157          ? extensions::GetLaunchType(extensions::ExtensionPrefs::Get(profile_),
158                                      GetExtension())
159          : extensions::LAUNCH_TYPE_WINDOW;
160
161  return !is_platform_app_ && extension_id_ != extension_misc::kChromeAppId &&
162      (!streamlined_hosted_apps ||
163       launch_type != extensions::LAUNCH_TYPE_WINDOW);
164}
165
166void ExtensionAppItem::Reload() {
167  const Extension* extension = GetExtension();
168  bool is_installing = !extension;
169  SetIsInstalling(is_installing);
170  if (is_installing) {
171    SetName(extension_name_);
172    UpdateIcon();
173    return;
174  }
175  SetNameAndShortName(extension->name(), extension->short_name());
176  LoadImage(extension);
177}
178
179void ExtensionAppItem::UpdateIcon() {
180  gfx::ImageSkia icon = installing_icon_;
181
182  // Use the app icon if the app exists. Turn the image greyscale if the app is
183  // not launchable.
184  if (GetExtension()) {
185    icon = icon_->image_skia();
186    const bool enabled = extensions::util::IsAppLaunchable(extension_id_,
187                                                           profile_);
188    if (!enabled) {
189      const color_utils::HSL shift = {-1, 0, 0.6};
190      icon = gfx::ImageSkiaOperations::CreateHSLShiftedImage(icon, shift);
191    }
192
193    if (GetExtension()->from_bookmark())
194      icon = gfx::ImageSkia(new RoundedCornersImageSource(icon), icon.size());
195  }
196  // Paint the shortcut overlay if necessary.
197  has_overlay_ = NeedsOverlay();
198  if (has_overlay_)
199    icon = gfx::ImageSkia(new ShortcutOverlayImageSource(icon), icon.size());
200
201  SetIcon(icon, true);
202}
203
204void ExtensionAppItem::Move(const ExtensionAppItem* prev,
205                            const ExtensionAppItem* next) {
206  if (!prev && !next)
207    return;  // No reordering necessary
208
209  extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
210  extensions::AppSorting* sorting = GetAppSorting(profile_);
211
212  syncer::StringOrdinal page;
213  std::string prev_id, next_id;
214  if (!prev) {
215    next_id = next->extension_id();
216    page = sorting->GetPageOrdinal(next_id);
217  } else if (!next) {
218    prev_id = prev->extension_id();
219    page = sorting->GetPageOrdinal(prev_id);
220  } else {
221    prev_id = prev->extension_id();
222    page = sorting->GetPageOrdinal(prev_id);
223    // Only set |next_id| if on the same page, otherwise just insert after prev.
224    if (page.Equals(sorting->GetPageOrdinal(next->extension_id())))
225      next_id = next->extension_id();
226  }
227  prefs->SetAppDraggedByUser(extension_id_);
228  sorting->SetPageOrdinal(extension_id_, page);
229  sorting->OnExtensionMoved(extension_id_, prev_id, next_id);
230  UpdatePositionFromExtensionOrdering();
231}
232
233const Extension* ExtensionAppItem::GetExtension() const {
234  const ExtensionService* service =
235      extensions::ExtensionSystem::Get(profile_)->extension_service();
236  const Extension* extension = service->GetInstalledExtension(extension_id_);
237  return extension;
238}
239
240void ExtensionAppItem::LoadImage(const Extension* extension) {
241  icon_.reset(new extensions::IconImage(
242      profile_,
243      extension,
244      extensions::IconsInfo::GetIcons(extension),
245      extension_misc::EXTENSION_ICON_MEDIUM,
246      extensions::util::GetDefaultAppIcon(),
247      this));
248  UpdateIcon();
249}
250
251bool ExtensionAppItem::RunExtensionEnableFlow() {
252  if (extensions::util::IsAppLaunchableWithoutEnabling(extension_id_, profile_))
253    return false;
254
255  if (!extension_enable_flow_) {
256    extension_enable_flow_controller_ = GetController();
257    extension_enable_flow_controller_->OnShowChildDialog();
258
259    extension_enable_flow_.reset(new ExtensionEnableFlow(
260        profile_, extension_id_, this));
261    extension_enable_flow_->StartForNativeWindow(
262        extension_enable_flow_controller_->GetAppListWindow());
263  }
264  return true;
265}
266
267void ExtensionAppItem::Launch(int event_flags) {
268  // |extension| could be NULL when it is being unloaded for updating.
269  const Extension* extension = GetExtension();
270  if (!extension)
271    return;
272
273  // Don't auto-enable apps that cannot be launched.
274  if (!extensions::util::IsAppLaunchable(extension_id_, profile_))
275    return;
276
277  if (RunExtensionEnableFlow())
278    return;
279
280  GetController()->LaunchApp(profile_,
281                             extension,
282                             AppListControllerDelegate::LAUNCH_FROM_APP_LIST,
283                             event_flags);
284}
285
286void ExtensionAppItem::OnExtensionIconImageChanged(
287    extensions::IconImage* image) {
288  DCHECK(icon_.get() == image);
289  UpdateIcon();
290}
291
292void ExtensionAppItem::ExtensionEnableFlowFinished() {
293  extension_enable_flow_.reset();
294  extension_enable_flow_controller_->OnCloseChildDialog();
295  extension_enable_flow_controller_ = NULL;
296
297  // Automatically launch app after enabling.
298  Launch(ui::EF_NONE);
299}
300
301void ExtensionAppItem::ExtensionEnableFlowAborted(bool user_initiated) {
302  extension_enable_flow_.reset();
303  extension_enable_flow_controller_->OnCloseChildDialog();
304  extension_enable_flow_controller_ = NULL;
305}
306
307void ExtensionAppItem::Activate(int event_flags) {
308  // |extension| could be NULL when it is being unloaded for updating.
309  const Extension* extension = GetExtension();
310  if (!extension)
311    return;
312
313  // Don't auto-enable apps that cannot be launched.
314  if (!extensions::util::IsAppLaunchable(extension_id_, profile_))
315    return;
316
317  if (RunExtensionEnableFlow())
318    return;
319
320  content::RecordAction(base::UserMetricsAction("AppList_ClickOnApp"));
321  CoreAppLauncherHandler::RecordAppListMainLaunch(extension);
322  GetController()->ActivateApp(profile_,
323                               extension,
324                               AppListControllerDelegate::LAUNCH_FROM_APP_LIST,
325                               event_flags);
326}
327
328ui::MenuModel* ExtensionAppItem::GetContextMenuModel() {
329  context_menu_.reset(new app_list::AppContextMenu(
330      this, profile_, extension_id_, GetController()));
331  context_menu_->set_is_platform_app(is_platform_app_);
332  if (IsInFolder())
333    context_menu_->set_is_in_folder(true);
334  return context_menu_->GetMenuModel();
335}
336
337void ExtensionAppItem::OnExtensionPreferenceChanged() {
338  if (has_overlay_ != NeedsOverlay())
339    UpdateIcon();
340}
341
342// static
343const char ExtensionAppItem::kItemType[] = "ExtensionAppItem";
344
345const char* ExtensionAppItem::GetItemType() const {
346  return ExtensionAppItem::kItemType;
347}
348
349void ExtensionAppItem::ExecuteLaunchCommand(int event_flags) {
350  Launch(event_flags);
351}
352
353void ExtensionAppItem::UpdatePositionFromExtensionOrdering() {
354  const syncer::StringOrdinal& page =
355      GetAppSorting(profile_)->GetPageOrdinal(extension_id_);
356  const syncer::StringOrdinal& launch =
357     GetAppSorting(profile_)->GetAppLaunchOrdinal(extension_id_);
358  set_position(syncer::StringOrdinal(
359      page.ToInternalValue() + launch.ToInternalValue()));
360}
361
362AppListControllerDelegate* ExtensionAppItem::GetController() {
363  return AppListService::Get(chrome::GetActiveDesktop())->
364      GetControllerDelegate();
365}
366