app_launcher_handler.cc revision 90dce4d38c5ff5333bea97d859d4e484e27edf0c
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/webui/ntp/app_launcher_handler.h"
6
7#include <vector>
8
9#include "apps/pref_names.h"
10#include "base/auto_reset.h"
11#include "base/bind.h"
12#include "base/bind_helpers.h"
13#include "base/i18n/rtl.h"
14#include "base/metrics/field_trial.h"
15#include "base/metrics/histogram.h"
16#include "base/prefs/pref_service.h"
17#include "base/utf_string_conversions.h"
18#include "base/values.h"
19#include "chrome/browser/browser_process.h"
20#include "chrome/browser/extensions/crx_installer.h"
21#include "chrome/browser/extensions/extension_prefs.h"
22#include "chrome/browser/extensions/extension_service.h"
23#include "chrome/browser/extensions/extension_sorting.h"
24#include "chrome/browser/extensions/extension_system.h"
25#include "chrome/browser/extensions/management_policy.h"
26#include "chrome/browser/favicon/favicon_service_factory.h"
27#include "chrome/browser/favicon/favicon_types.h"
28#include "chrome/browser/prefs/scoped_user_pref_update.h"
29#include "chrome/browser/profiles/profile.h"
30#include "chrome/browser/ui/browser_finder.h"
31#include "chrome/browser/ui/browser_tabstrip.h"
32#include "chrome/browser/ui/browser_window.h"
33#include "chrome/browser/ui/extensions/application_launch.h"
34#include "chrome/browser/ui/extensions/extension_enable_flow.h"
35#include "chrome/browser/ui/tabs/tab_strip_model.h"
36#include "chrome/browser/ui/webui/extensions/extension_icon_source.h"
37#include "chrome/browser/ui/webui/ntp/new_tab_ui.h"
38#include "chrome/common/chrome_notification_types.h"
39#include "chrome/common/extensions/extension.h"
40#include "chrome/common/extensions/extension_constants.h"
41#include "chrome/common/extensions/extension_icon_set.h"
42#include "chrome/common/pref_names.h"
43#include "chrome/common/url_constants.h"
44#include "chrome/common/web_application_info.h"
45#include "components/user_prefs/pref_registry_syncable.h"
46#include "content/public/browser/notification_service.h"
47#include "content/public/browser/web_ui.h"
48#include "content/public/common/favicon_url.h"
49#include "googleurl/src/gurl.h"
50#include "grit/browser_resources.h"
51#include "grit/generated_resources.h"
52#include "net/base/escape.h"
53#include "ui/base/l10n/l10n_util.h"
54#include "ui/gfx/favicon_size.h"
55#include "ui/webui/web_ui_util.h"
56
57using chrome::AppLaunchParams;
58using chrome::OpenApplication;
59using content::WebContents;
60using extensions::CrxInstaller;
61using extensions::Extension;
62using extensions::ExtensionPrefs;
63
64namespace {
65
66bool ShouldDisplayInNewTabPage(const Extension* app, PrefService* prefs) {
67  bool blocked_by_policy =
68    (app->id() == extension_misc::kWebStoreAppId ||
69     app->id() == extension_misc::kEnterpriseWebStoreAppId) &&
70    prefs->GetBoolean(prefs::kHideWebStoreIcon);
71  return app->ShouldDisplayInNewTabPage() && !blocked_by_policy;
72}
73
74}  // namespace
75
76const net::UnescapeRule::Type kUnescapeRules =
77    net::UnescapeRule::NORMAL | net::UnescapeRule::URL_SPECIAL_CHARS;
78
79AppLauncherHandler::AppInstallInfo::AppInstallInfo() {}
80
81AppLauncherHandler::AppInstallInfo::~AppInstallInfo() {}
82
83AppLauncherHandler::AppLauncherHandler(ExtensionService* extension_service)
84    : extension_service_(extension_service),
85      ignore_changes_(false),
86      attempted_bookmark_app_install_(false),
87      has_loaded_apps_(false) {
88}
89
90AppLauncherHandler::~AppLauncherHandler() {}
91
92void AppLauncherHandler::CreateAppInfo(
93    const Extension* extension,
94    ExtensionService* service,
95    DictionaryValue* value) {
96  value->Clear();
97
98  // The Extension class 'helpfully' wraps bidi control characters that
99  // impede our ability to determine directionality.
100  string16 name = UTF8ToUTF16(extension->name());
101  base::i18n::UnadjustStringForLocaleDirection(&name);
102  NewTabUI::SetUrlTitleAndDirection(value, name, extension->GetFullLaunchURL());
103
104  bool enabled = service->IsExtensionEnabled(extension->id()) &&
105      !service->GetTerminatedExtension(extension->id());
106  extension->GetBasicInfo(enabled, value);
107
108  value->SetBoolean("mayDisable", extensions::ExtensionSystem::Get(
109      service->profile())->management_policy()->UserMayModifySettings(
110      extension, NULL));
111
112  bool icon_big_exists = true;
113  // Instead of setting grayscale here, we do it in apps_page.js.
114  GURL icon_big =
115      ExtensionIconSource::GetIconURL(extension,
116                                      extension_misc::EXTENSION_ICON_LARGE,
117                                      ExtensionIconSet::MATCH_BIGGER,
118                                      false, &icon_big_exists);
119  value->SetString("icon_big", icon_big.spec());
120  value->SetBoolean("icon_big_exists", icon_big_exists);
121  bool icon_small_exists = true;
122  GURL icon_small =
123      ExtensionIconSource::GetIconURL(extension,
124                                      extension_misc::EXTENSION_ICON_BITTY,
125                                      ExtensionIconSet::MATCH_BIGGER,
126                                      false, &icon_small_exists);
127  value->SetString("icon_small", icon_small.spec());
128  value->SetBoolean("icon_small_exists", icon_small_exists);
129  value->SetInteger("launch_container", extension->launch_container());
130  ExtensionPrefs* prefs = service->extension_prefs();
131  value->SetInteger("launch_type",
132      prefs->GetLaunchType(extension,
133                           ExtensionPrefs::LAUNCH_DEFAULT));
134  value->SetBoolean("is_component",
135                    extension->location() == extensions::Manifest::COMPONENT);
136  value->SetBoolean("is_webstore",
137      extension->id() == extension_misc::kWebStoreAppId);
138
139  ExtensionSorting* sorting = prefs->extension_sorting();
140  syncer::StringOrdinal page_ordinal = sorting->GetPageOrdinal(extension->id());
141  if (!page_ordinal.IsValid()) {
142    // Make sure every app has a page ordinal (some predate the page ordinal).
143    // The webstore app should be on the first page.
144    page_ordinal = extension->id() == extension_misc::kWebStoreAppId ?
145        sorting->CreateFirstAppPageOrdinal() :
146        sorting->GetNaturalAppPageOrdinal();
147    sorting->SetPageOrdinal(extension->id(), page_ordinal);
148  }
149  value->SetInteger("page_index",
150      sorting->PageStringOrdinalAsInteger(page_ordinal));
151
152  syncer::StringOrdinal app_launch_ordinal =
153      sorting->GetAppLaunchOrdinal(extension->id());
154  if (!app_launch_ordinal.IsValid()) {
155    // Make sure every app has a launch ordinal (some predate the launch
156    // ordinal). The webstore's app launch ordinal is always set to the first
157    // position.
158    app_launch_ordinal = extension->id() == extension_misc::kWebStoreAppId ?
159        sorting->CreateFirstAppLaunchOrdinal(page_ordinal) :
160        sorting->CreateNextAppLaunchOrdinal(page_ordinal);
161    sorting->SetAppLaunchOrdinal(extension->id(), app_launch_ordinal);
162  }
163  value->SetString("app_launch_ordinal", app_launch_ordinal.ToInternalValue());
164}
165
166void AppLauncherHandler::RegisterMessages() {
167  registrar_.Add(this, chrome::NOTIFICATION_APP_INSTALLED_TO_NTP,
168      content::Source<WebContents>(web_ui()->GetWebContents()));
169
170  // Some tests don't have a local state.
171  if (g_browser_process->local_state()) {
172    local_state_pref_change_registrar_.Init(g_browser_process->local_state());
173    local_state_pref_change_registrar_.Add(
174        apps::prefs::kShowAppLauncherPromo,
175        base::Bind(&AppLauncherHandler::OnLocalStatePreferenceChanged,
176                   base::Unretained(this)));
177  }
178  web_ui()->RegisterMessageCallback("getApps",
179      base::Bind(&AppLauncherHandler::HandleGetApps,
180                 base::Unretained(this)));
181  web_ui()->RegisterMessageCallback("launchApp",
182      base::Bind(&AppLauncherHandler::HandleLaunchApp,
183                 base::Unretained(this)));
184  web_ui()->RegisterMessageCallback("setLaunchType",
185      base::Bind(&AppLauncherHandler::HandleSetLaunchType,
186                 base::Unretained(this)));
187  web_ui()->RegisterMessageCallback("uninstallApp",
188      base::Bind(&AppLauncherHandler::HandleUninstallApp,
189                 base::Unretained(this)));
190  web_ui()->RegisterMessageCallback("createAppShortcut",
191      base::Bind(&AppLauncherHandler::HandleCreateAppShortcut,
192                 base::Unretained(this)));
193  web_ui()->RegisterMessageCallback("reorderApps",
194      base::Bind(&AppLauncherHandler::HandleReorderApps,
195                 base::Unretained(this)));
196  web_ui()->RegisterMessageCallback("setPageIndex",
197      base::Bind(&AppLauncherHandler::HandleSetPageIndex,
198                 base::Unretained(this)));
199  web_ui()->RegisterMessageCallback("saveAppPageName",
200      base::Bind(&AppLauncherHandler::HandleSaveAppPageName,
201                 base::Unretained(this)));
202  web_ui()->RegisterMessageCallback("generateAppForLink",
203      base::Bind(&AppLauncherHandler::HandleGenerateAppForLink,
204                 base::Unretained(this)));
205  web_ui()->RegisterMessageCallback("recordAppLaunchByURL",
206      base::Bind(&AppLauncherHandler::HandleRecordAppLaunchByUrl,
207                 base::Unretained(this)));
208  web_ui()->RegisterMessageCallback("stopShowingAppLauncherPromo",
209      base::Bind(&AppLauncherHandler::StopShowingAppLauncherPromo,
210                 base::Unretained(this)));
211}
212
213void AppLauncherHandler::Observe(int type,
214                                 const content::NotificationSource& source,
215                                 const content::NotificationDetails& details) {
216  if (type == chrome::NOTIFICATION_APP_INSTALLED_TO_NTP) {
217    highlight_app_id_ = *content::Details<const std::string>(details).ptr();
218    if (has_loaded_apps_)
219      SetAppToBeHighlighted();
220    return;
221  }
222
223  if (ignore_changes_ || !has_loaded_apps_)
224    return;
225
226  switch (type) {
227    case chrome::NOTIFICATION_EXTENSION_LOADED: {
228      const Extension* extension =
229          content::Details<const Extension>(details).ptr();
230      if (!extension->is_app())
231        return;
232
233      scoped_ptr<DictionaryValue> app_info(GetAppInfo(extension));
234      if (app_info.get()) {
235        visible_apps_.insert(extension->id());
236
237        ExtensionPrefs* prefs = extension_service_->extension_prefs();
238        scoped_ptr<base::FundamentalValue> highlight(Value::CreateBooleanValue(
239              prefs->IsFromBookmark(extension->id()) &&
240              attempted_bookmark_app_install_));
241        attempted_bookmark_app_install_ = false;
242        web_ui()->CallJavascriptFunction(
243            "ntp.appAdded", *app_info, *highlight);
244      }
245
246      break;
247    }
248    case chrome::NOTIFICATION_EXTENSION_UNLOADED: {
249      const Extension* extension =
250          content::Details<extensions::UnloadedExtensionInfo>(
251              details)->extension;
252      if (!extension->is_app())
253        return;
254
255      scoped_ptr<DictionaryValue> app_info(GetAppInfo(extension));
256      scoped_ptr<base::FundamentalValue> uninstall_value(
257          Value::CreateBooleanValue(
258              content::Details<extensions::UnloadedExtensionInfo>(
259                  details)->reason == extension_misc::UNLOAD_REASON_UNINSTALL));
260      if (app_info.get()) {
261        visible_apps_.erase(extension->id());
262
263        scoped_ptr<base::FundamentalValue> from_page(
264            Value::CreateBooleanValue(!extension_id_prompting_.empty()));
265        web_ui()->CallJavascriptFunction(
266            "ntp.appRemoved", *app_info, *uninstall_value, *from_page);
267      }
268      break;
269    }
270    case chrome::NOTIFICATION_EXTENSION_LAUNCHER_REORDERED: {
271      const std::string* id =
272          content::Details<const std::string>(details).ptr();
273      if (id) {
274        const Extension* extension =
275            extension_service_->GetExtensionById(*id, false);
276        DictionaryValue app_info;
277        CreateAppInfo(extension,
278                      extension_service_,
279                      &app_info);
280        web_ui()->CallJavascriptFunction("ntp.appMoved", app_info);
281      } else {
282        HandleGetApps(NULL);
283      }
284      break;
285    }
286    // The promo may not load until a couple seconds after the first NTP view,
287    // so we listen for the load notification and notify the NTP when ready.
288    case chrome::NOTIFICATION_WEB_STORE_PROMO_LOADED:
289      // TODO(estade): Try to get rid of this inefficient operation.
290      HandleGetApps(NULL);
291      break;
292    case chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR: {
293      CrxInstaller* crx_installer = content::Source<CrxInstaller>(source).ptr();
294      if (!Profile::FromWebUI(web_ui())->IsSameProfile(
295              crx_installer->profile())) {
296        return;
297      }
298      // Fall through.
299    }
300    case chrome::NOTIFICATION_EXTENSION_LOAD_ERROR: {
301      attempted_bookmark_app_install_ = false;
302      break;
303    }
304    default:
305      NOTREACHED();
306  }
307}
308
309void AppLauncherHandler::FillAppDictionary(DictionaryValue* dictionary) {
310  // CreateAppInfo and ClearOrdinals can change the extension prefs.
311  base::AutoReset<bool> auto_reset(&ignore_changes_, true);
312
313  ListValue* list = new ListValue();
314  PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs();
315
316  for (std::set<std::string>::iterator it = visible_apps_.begin();
317       it != visible_apps_.end(); ++it) {
318    const Extension* extension = extension_service_->GetInstalledExtension(*it);
319    if (extension && ShouldDisplayInNewTabPage(extension, prefs)) {
320      DictionaryValue* app_info = GetAppInfo(extension);
321      list->Append(app_info);
322    }
323  }
324
325  dictionary->Set("apps", list);
326
327  // TODO(estade): remove these settings when the old NTP is removed. The new
328  // NTP does it in js.
329#if defined(OS_MACOSX)
330  // App windows are not yet implemented on mac.
331  dictionary->SetBoolean("disableAppWindowLaunch", true);
332  dictionary->SetBoolean("disableCreateAppShortcut", true);
333#endif
334
335#if defined(OS_CHROMEOS)
336  // Making shortcut does not make sense on ChromeOS because it does not have
337  // a desktop.
338  dictionary->SetBoolean("disableCreateAppShortcut", true);
339#endif
340
341  const ListValue* app_page_names = prefs->GetList(prefs::kNtpAppPageNames);
342  if (!app_page_names || !app_page_names->GetSize()) {
343    ListPrefUpdate update(prefs, prefs::kNtpAppPageNames);
344    ListValue* list = update.Get();
345    list->Set(0, Value::CreateStringValue(
346        l10n_util::GetStringUTF16(IDS_APP_DEFAULT_PAGE_NAME)));
347    dictionary->Set("appPageNames",
348                    static_cast<ListValue*>(list->DeepCopy()));
349  } else {
350    dictionary->Set("appPageNames",
351                    static_cast<ListValue*>(app_page_names->DeepCopy()));
352  }
353}
354
355DictionaryValue* AppLauncherHandler::GetAppInfo(const Extension* extension) {
356  DictionaryValue* app_info = new DictionaryValue();
357  // CreateAppInfo can change the extension prefs.
358  base::AutoReset<bool> auto_reset(&ignore_changes_, true);
359  CreateAppInfo(extension,
360                extension_service_,
361                app_info);
362  return app_info;
363}
364
365void AppLauncherHandler::HandleGetApps(const ListValue* args) {
366  DictionaryValue dictionary;
367
368  // Tell the client whether to show the promo for this view. We don't do this
369  // in the case of PREF_CHANGED because:
370  //
371  // a) At that point in time, depending on the pref that changed, it can look
372  //    like the set of apps installed has changed, and we will mark the promo
373  //    expired.
374  // b) Conceptually, it doesn't really make sense to count a
375  //    prefchange-triggered refresh as a promo 'view'.
376  Profile* profile = Profile::FromWebUI(web_ui());
377
378  // The first time we load the apps we must add all current app to the list
379  // of apps visible on the NTP.
380  if (!has_loaded_apps_) {
381    const ExtensionSet* extensions = extension_service_->extensions();
382    for (ExtensionSet::const_iterator it = extensions->begin();
383         it != extensions->end(); ++it) {
384      visible_apps_.insert((*it)->id());
385    }
386
387    extensions = extension_service_->disabled_extensions();
388    for (ExtensionSet::const_iterator it = extensions->begin();
389         it != extensions->end(); ++it) {
390      visible_apps_.insert((*it)->id());
391    }
392
393    extensions = extension_service_->terminated_extensions();
394    for (ExtensionSet::const_iterator it = extensions->begin();
395         it != extensions->end(); ++it) {
396      visible_apps_.insert((*it)->id());
397    }
398  }
399
400  SetAppToBeHighlighted();
401  FillAppDictionary(&dictionary);
402  web_ui()->CallJavascriptFunction("ntp.getAppsCallback", dictionary);
403
404  // First time we get here we set up the observer so that we can tell update
405  // the apps as they change.
406  if (!has_loaded_apps_) {
407    base::Closure callback = base::Bind(
408        &AppLauncherHandler::OnExtensionPreferenceChanged,
409        base::Unretained(this));
410    extension_pref_change_registrar_.Init(
411        extension_service_->extension_prefs()->pref_service());
412    extension_pref_change_registrar_.Add(ExtensionPrefs::kExtensionsPref,
413                                         callback);
414    extension_pref_change_registrar_.Add(prefs::kNtpAppPageNames, callback);
415
416    registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED,
417        content::Source<Profile>(profile));
418    registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED,
419        content::Source<Profile>(profile));
420    registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LAUNCHER_REORDERED,
421        content::Source<ExtensionSorting>(
422            extension_service_->extension_prefs()->extension_sorting()));
423    registrar_.Add(this, chrome::NOTIFICATION_WEB_STORE_PROMO_LOADED,
424        content::Source<Profile>(profile));
425    registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR,
426        content::Source<CrxInstaller>(NULL));
427    registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOAD_ERROR,
428        content::Source<Profile>(profile));
429  }
430
431  has_loaded_apps_ = true;
432}
433
434void AppLauncherHandler::HandleLaunchApp(const ListValue* args) {
435  std::string extension_id;
436  CHECK(args->GetString(0, &extension_id));
437  double source = -1.0;
438  CHECK(args->GetDouble(1, &source));
439  std::string url;
440  if (args->GetSize() > 2)
441    CHECK(args->GetString(2, &url));
442
443  extension_misc::AppLaunchBucket launch_bucket =
444      static_cast<extension_misc::AppLaunchBucket>(
445          static_cast<int>(source));
446  CHECK(launch_bucket >= 0 &&
447        launch_bucket < extension_misc::APP_LAUNCH_BUCKET_BOUNDARY);
448
449  const Extension* extension =
450      extension_service_->GetExtensionById(extension_id, false);
451
452  // Prompt the user to re-enable the application if disabled.
453  if (!extension) {
454    PromptToEnableApp(extension_id);
455    return;
456  }
457
458  Profile* profile = extension_service_->profile();
459
460  WindowOpenDisposition disposition = args->GetSize() > 3 ?
461        webui::GetDispositionFromClick(args, 3) : CURRENT_TAB;
462  if (extension_id != extension_misc::kWebStoreAppId) {
463    CHECK_NE(launch_bucket, extension_misc::APP_LAUNCH_BUCKET_INVALID);
464    RecordAppLaunchType(launch_bucket, extension->GetType());
465  } else {
466    RecordWebStoreLaunch();
467  }
468
469  if (disposition == NEW_FOREGROUND_TAB || disposition == NEW_BACKGROUND_TAB ||
470      disposition == NEW_WINDOW) {
471    // TODO(jamescook): Proper support for background tabs.
472    AppLaunchParams params(profile, extension,
473                           disposition == NEW_WINDOW ?
474                               extension_misc::LAUNCH_WINDOW :
475                               extension_misc::LAUNCH_TAB,
476                           disposition);
477    params.override_url = GURL(url);
478    OpenApplication(params);
479  } else {
480    // To give a more "launchy" experience when using the NTP launcher, we close
481    // it automatically.
482    Browser* browser = chrome::FindBrowserWithWebContents(
483        web_ui()->GetWebContents());
484    WebContents* old_contents = NULL;
485    if (browser)
486      old_contents = browser->tab_strip_model()->GetActiveWebContents();
487
488    AppLaunchParams params(profile, extension,
489                           old_contents ? CURRENT_TAB : NEW_FOREGROUND_TAB);
490    params.override_url = GURL(url);
491    WebContents* new_contents = OpenApplication(params);
492
493    // This will also destroy the handler, so do not perform any actions after.
494    if (new_contents != old_contents && browser &&
495        browser->tab_strip_model()->count() > 1) {
496      chrome::CloseWebContents(browser, old_contents, true);
497    }
498  }
499}
500
501void AppLauncherHandler::HandleSetLaunchType(const ListValue* args) {
502  std::string extension_id;
503  double launch_type;
504  CHECK(args->GetString(0, &extension_id));
505  CHECK(args->GetDouble(1, &launch_type));
506
507  const Extension* extension =
508      extension_service_->GetExtensionById(extension_id, true);
509  if (!extension)
510    return;
511
512  // Don't update the page; it already knows about the launch type change.
513  base::AutoReset<bool> auto_reset(&ignore_changes_, true);
514
515  extension_service_->extension_prefs()->SetLaunchType(
516      extension_id,
517      static_cast<ExtensionPrefs::LaunchType>(
518          static_cast<int>(launch_type)));
519}
520
521void AppLauncherHandler::HandleUninstallApp(const ListValue* args) {
522  std::string extension_id;
523  CHECK(args->GetString(0, &extension_id));
524
525  const Extension* extension = extension_service_->GetExtensionById(
526      extension_id, true);
527  if (!extension)
528    return;
529
530  if (!extensions::ExtensionSystem::Get(extension_service_->profile())->
531          management_policy()->UserMayModifySettings(extension, NULL)) {
532    LOG(ERROR) << "Attempt to uninstall an extension that is non-usermanagable "
533               << "was made. Extension id : " << extension->id();
534    return;
535  }
536  if (!extension_id_prompting_.empty())
537    return;  // Only one prompt at a time.
538
539  extension_id_prompting_ = extension_id;
540
541  bool dont_confirm = false;
542  if (args->GetBoolean(1, &dont_confirm) && dont_confirm) {
543    base::AutoReset<bool> auto_reset(&ignore_changes_, true);
544    ExtensionUninstallAccepted();
545  } else {
546    GetExtensionUninstallDialog()->ConfirmUninstall(extension);
547  }
548}
549
550void AppLauncherHandler::HandleCreateAppShortcut(const ListValue* args) {
551  std::string extension_id;
552  CHECK(args->GetString(0, &extension_id));
553
554  const Extension* extension =
555      extension_service_->GetExtensionById(extension_id, true);
556  if (!extension)
557    return;
558
559  Browser* browser = chrome::FindBrowserWithWebContents(
560        web_ui()->GetWebContents());
561  browser->window()->ShowCreateChromeAppShortcutsDialog(
562      browser->profile(), extension);
563}
564
565void AppLauncherHandler::HandleReorderApps(const ListValue* args) {
566  CHECK(args->GetSize() == 2);
567
568  std::string dragged_app_id;
569  const ListValue* app_order;
570  CHECK(args->GetString(0, &dragged_app_id));
571  CHECK(args->GetList(1, &app_order));
572
573  std::string predecessor_to_moved_ext;
574  std::string successor_to_moved_ext;
575  for (size_t i = 0; i < app_order->GetSize(); ++i) {
576    std::string value;
577    if (app_order->GetString(i, &value) && value == dragged_app_id) {
578      if (i > 0)
579        CHECK(app_order->GetString(i - 1, &predecessor_to_moved_ext));
580      if (i + 1 < app_order->GetSize())
581        CHECK(app_order->GetString(i + 1, &successor_to_moved_ext));
582      break;
583    }
584  }
585
586  // Don't update the page; it already knows the apps have been reordered.
587  base::AutoReset<bool> auto_reset(&ignore_changes_, true);
588  extension_service_->extension_prefs()->SetAppDraggedByUser(dragged_app_id);
589  extension_service_->OnExtensionMoved(dragged_app_id,
590                                       predecessor_to_moved_ext,
591                                       successor_to_moved_ext);
592}
593
594void AppLauncherHandler::HandleSetPageIndex(const ListValue* args) {
595  ExtensionSorting* extension_sorting =
596      extension_service_->extension_prefs()->extension_sorting();
597
598  std::string extension_id;
599  double page_index;
600  CHECK(args->GetString(0, &extension_id));
601  CHECK(args->GetDouble(1, &page_index));
602  const syncer::StringOrdinal& page_ordinal =
603      extension_sorting->PageIntegerAsStringOrdinal(
604          static_cast<size_t>(page_index));
605
606  // Don't update the page; it already knows the apps have been reordered.
607  base::AutoReset<bool> auto_reset(&ignore_changes_, true);
608  extension_sorting->SetPageOrdinal(extension_id, page_ordinal);
609}
610
611void AppLauncherHandler::HandleSaveAppPageName(const ListValue* args) {
612  string16 name;
613  CHECK(args->GetString(0, &name));
614
615  double page_index;
616  CHECK(args->GetDouble(1, &page_index));
617
618  base::AutoReset<bool> auto_reset(&ignore_changes_, true);
619  PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs();
620  ListPrefUpdate update(prefs, prefs::kNtpAppPageNames);
621  ListValue* list = update.Get();
622  list->Set(static_cast<size_t>(page_index), Value::CreateStringValue(name));
623}
624
625void AppLauncherHandler::HandleGenerateAppForLink(const ListValue* args) {
626  std::string url;
627  CHECK(args->GetString(0, &url));
628  GURL launch_url(url);
629
630  string16 title;
631  CHECK(args->GetString(1, &title));
632
633  double page_index;
634  CHECK(args->GetDouble(2, &page_index));
635  ExtensionSorting* extension_sorting =
636      extension_service_->extension_prefs()->extension_sorting();
637  const syncer::StringOrdinal& page_ordinal =
638      extension_sorting->PageIntegerAsStringOrdinal(
639          static_cast<size_t>(page_index));
640
641  Profile* profile = Profile::FromWebUI(web_ui());
642  FaviconService* favicon_service =
643      FaviconServiceFactory::GetForProfile(profile, Profile::EXPLICIT_ACCESS);
644  if (!favicon_service) {
645    LOG(ERROR) << "No favicon service";
646    return;
647  }
648
649  scoped_ptr<AppInstallInfo> install_info(new AppInstallInfo());
650  install_info->is_bookmark_app = true;
651  install_info->title = title;
652  install_info->app_url = launch_url;
653  install_info->page_ordinal = page_ordinal;
654
655  favicon_service->GetFaviconImageForURL(
656      FaviconService::FaviconForURLParams(profile,
657                                          launch_url,
658                                          chrome::FAVICON,
659                                          gfx::kFaviconSize),
660      base::Bind(&AppLauncherHandler::OnFaviconForApp,
661                 base::Unretained(this),
662                 base::Passed(&install_info)),
663      &cancelable_task_tracker_);
664}
665
666void AppLauncherHandler::HandleRecordAppLaunchByUrl(
667    const base::ListValue* args) {
668  std::string url;
669  CHECK(args->GetString(0, &url));
670  double source;
671  CHECK(args->GetDouble(1, &source));
672
673  extension_misc::AppLaunchBucket bucket =
674      static_cast<extension_misc::AppLaunchBucket>(static_cast<int>(source));
675  CHECK(source < extension_misc::APP_LAUNCH_BUCKET_BOUNDARY);
676
677  RecordAppLaunchByUrl(Profile::FromWebUI(web_ui()), url, bucket);
678}
679
680void AppLauncherHandler::StopShowingAppLauncherPromo(
681    const base::ListValue* args) {
682  g_browser_process->local_state()->SetBoolean(
683      apps::prefs::kShowAppLauncherPromo, false);
684}
685
686void AppLauncherHandler::OnFaviconForApp(
687    scoped_ptr<AppInstallInfo> install_info,
688    const chrome::FaviconImageResult& image_result) {
689  scoped_ptr<WebApplicationInfo> web_app(new WebApplicationInfo());
690  web_app->is_bookmark_app = install_info->is_bookmark_app;
691  web_app->title = install_info->title;
692  web_app->app_url = install_info->app_url;
693  web_app->urls.push_back(install_info->app_url);
694
695  if (!image_result.image.IsEmpty()) {
696    WebApplicationInfo::IconInfo icon;
697    icon.data = image_result.image.AsBitmap();
698    icon.width = icon.data.width();
699    icon.height = icon.data.height();
700    web_app->icons.push_back(icon);
701  }
702
703  scoped_refptr<CrxInstaller> installer(
704      CrxInstaller::Create(extension_service_, NULL));
705  installer->set_error_on_unsupported_requirements(true);
706  installer->set_page_ordinal(install_info->page_ordinal);
707  installer->InstallWebApp(*web_app);
708  attempted_bookmark_app_install_ = true;
709}
710
711void AppLauncherHandler::SetAppToBeHighlighted() {
712  if (highlight_app_id_.empty())
713    return;
714
715  StringValue app_id(highlight_app_id_);
716  web_ui()->CallJavascriptFunction("ntp.setAppToBeHighlighted", app_id);
717  highlight_app_id_.clear();
718}
719
720void AppLauncherHandler::OnExtensionPreferenceChanged() {
721  DictionaryValue dictionary;
722  FillAppDictionary(&dictionary);
723  web_ui()->CallJavascriptFunction("ntp.appsPrefChangeCallback", dictionary);
724}
725
726void AppLauncherHandler::OnLocalStatePreferenceChanged() {
727  web_ui()->CallJavascriptFunction(
728      "ntp.appLauncherPromoPrefChangeCallback",
729      base::FundamentalValue(g_browser_process->local_state()->GetBoolean(
730          apps::prefs::kShowAppLauncherPromo)));
731}
732
733// static
734void AppLauncherHandler::RegisterUserPrefs(
735    user_prefs::PrefRegistrySyncable* registry) {
736  registry->RegisterListPref(prefs::kNtpAppPageNames,
737                             user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
738}
739
740void AppLauncherHandler::CleanupAfterUninstall() {
741  extension_id_prompting_.clear();
742}
743
744// static
745void AppLauncherHandler::RecordAppLaunchType(
746    extension_misc::AppLaunchBucket bucket,
747    extensions::Manifest::Type app_type) {
748  if (app_type == extensions::Manifest::TYPE_PLATFORM_APP) {
749    UMA_HISTOGRAM_ENUMERATION(extension_misc::kPlatformAppLaunchHistogram,
750        bucket, extension_misc::APP_LAUNCH_BUCKET_BOUNDARY);
751  } else {
752    UMA_HISTOGRAM_ENUMERATION(extension_misc::kAppLaunchHistogram,
753        bucket, extension_misc::APP_LAUNCH_BUCKET_BOUNDARY);
754  }
755}
756
757// static
758void AppLauncherHandler::RecordAppListSearchLaunch(const Extension* extension) {
759  extension_misc::AppLaunchBucket bucket =
760      extension_misc::APP_LAUNCH_APP_LIST_SEARCH;
761  if (extension->id() == extension_misc::kWebStoreAppId)
762    bucket = extension_misc::APP_LAUNCH_APP_LIST_SEARCH_WEBSTORE;
763  else if (extension->id() == extension_misc::kChromeAppId)
764    bucket = extension_misc::APP_LAUNCH_APP_LIST_SEARCH_CHROME;
765  AppLauncherHandler::RecordAppLaunchType(bucket, extension->GetType());
766}
767
768// static
769void AppLauncherHandler::RecordAppListMainLaunch(const Extension* extension) {
770  extension_misc::AppLaunchBucket bucket =
771      extension_misc::APP_LAUNCH_APP_LIST_MAIN;
772  if (extension->id() == extension_misc::kWebStoreAppId)
773    bucket = extension_misc::APP_LAUNCH_APP_LIST_MAIN_WEBSTORE;
774  else if (extension->id() == extension_misc::kChromeAppId)
775    bucket = extension_misc::APP_LAUNCH_APP_LIST_MAIN_CHROME;
776  AppLauncherHandler::RecordAppLaunchType(bucket, extension->GetType());
777}
778
779// static
780void AppLauncherHandler::RecordWebStoreLaunch() {
781  RecordAppLaunchType(extension_misc::APP_LAUNCH_NTP_WEBSTORE,
782      extensions::Manifest::TYPE_HOSTED_APP);
783}
784
785// static
786void AppLauncherHandler::RecordAppLaunchByUrl(
787    Profile* profile,
788    std::string escaped_url,
789    extension_misc::AppLaunchBucket bucket) {
790  CHECK(bucket != extension_misc::APP_LAUNCH_BUCKET_INVALID);
791
792  GURL url(net::UnescapeURLComponent(escaped_url, kUnescapeRules));
793  DCHECK(profile->GetExtensionService());
794  if (!profile->GetExtensionService()->IsInstalledApp(url))
795    return;
796
797  RecordAppLaunchType(bucket, extensions::Manifest::TYPE_HOSTED_APP);
798}
799
800void AppLauncherHandler::PromptToEnableApp(const std::string& extension_id) {
801  if (!extension_id_prompting_.empty())
802    return;  // Only one prompt at a time.
803
804  extension_id_prompting_ = extension_id;
805  extension_enable_flow_.reset(new ExtensionEnableFlow(
806      Profile::FromWebUI(web_ui()), extension_id, this));
807  extension_enable_flow_->StartForWebContents(web_ui()->GetWebContents());
808}
809
810void AppLauncherHandler::ExtensionUninstallAccepted() {
811  // Do the uninstall work here.
812  DCHECK(!extension_id_prompting_.empty());
813
814  // The extension can be uninstalled in another window while the UI was
815  // showing. Do nothing in that case.
816  const Extension* extension =
817      extension_service_->GetExtensionById(extension_id_prompting_, true);
818  if (!extension)
819    return;
820
821  extension_service_->UninstallExtension(extension_id_prompting_,
822                                         false /* external_uninstall */, NULL);
823  CleanupAfterUninstall();
824}
825
826void AppLauncherHandler::ExtensionUninstallCanceled() {
827  CleanupAfterUninstall();
828}
829
830void AppLauncherHandler::ExtensionEnableFlowFinished() {
831  DCHECK_EQ(extension_id_prompting_, extension_enable_flow_->extension_id());
832
833  // We bounce this off the NTP so the browser can update the apps icon.
834  // If we don't launch the app asynchronously, then the app's disabled
835  // icon disappears but isn't replaced by the enabled icon, making a poor
836  // visual experience.
837  StringValue app_id(extension_id_prompting_);
838  web_ui()->CallJavascriptFunction("ntp.launchAppAfterEnable", app_id);
839
840  extension_enable_flow_.reset();
841  extension_id_prompting_ = "";
842}
843
844void AppLauncherHandler::ExtensionEnableFlowAborted(bool user_initiated) {
845  DCHECK_EQ(extension_id_prompting_, extension_enable_flow_->extension_id());
846
847  // We record the histograms here because ExtensionUninstallCanceled is also
848  // called when the extension uninstall dialog is canceled.
849  const Extension* extension =
850      extension_service_->GetExtensionById(extension_id_prompting_, true);
851  std::string histogram_name = user_initiated ?
852      "Extensions.Permissions_ReEnableCancel" :
853      "Extensions.Permissions_ReEnableAbort";
854  ExtensionService::RecordPermissionMessagesHistogram(
855      extension, histogram_name.c_str());
856
857  extension_enable_flow_.reset();
858  CleanupAfterUninstall();
859}
860
861ExtensionUninstallDialog* AppLauncherHandler::GetExtensionUninstallDialog() {
862  if (!extension_uninstall_dialog_.get()) {
863    Browser* browser = chrome::FindBrowserWithWebContents(
864        web_ui()->GetWebContents());
865    extension_uninstall_dialog_.reset(
866        ExtensionUninstallDialog::Create(extension_service_->profile(),
867                                         browser, this));
868  }
869  return extension_uninstall_dialog_.get();
870}
871