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/new_tab_ui.h"
6
7#include <set>
8
9#include "base/i18n/rtl.h"
10#include "base/lazy_instance.h"
11#include "base/memory/scoped_ptr.h"
12#include "base/metrics/histogram.h"
13#include "base/prefs/pref_service.h"
14#include "base/strings/utf_string_conversions.h"
15#include "chrome/browser/chrome_notification_types.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/ui/webui/metrics_handler.h"
18#include "chrome/browser/ui/webui/ntp/app_launcher_handler.h"
19#include "chrome/browser/ui/webui/ntp/core_app_launcher_handler.h"
20#include "chrome/browser/ui/webui/ntp/favicon_webui_handler.h"
21#include "chrome/browser/ui/webui/ntp/foreign_session_handler.h"
22#include "chrome/browser/ui/webui/ntp/most_visited_handler.h"
23#include "chrome/browser/ui/webui/ntp/new_tab_page_handler.h"
24#include "chrome/browser/ui/webui/ntp/new_tab_page_sync_handler.h"
25#include "chrome/browser/ui/webui/ntp/ntp_login_handler.h"
26#include "chrome/browser/ui/webui/ntp/ntp_resource_cache.h"
27#include "chrome/browser/ui/webui/ntp/ntp_resource_cache_factory.h"
28#include "chrome/browser/ui/webui/ntp/ntp_user_data_logger.h"
29#include "chrome/browser/ui/webui/ntp/recently_closed_tabs_handler.h"
30#include "chrome/browser/ui/webui/ntp/suggestions_page_handler.h"
31#include "chrome/common/pref_names.h"
32#include "chrome/common/url_constants.h"
33#include "chrome/grit/generated_resources.h"
34#include "components/pref_registry/pref_registry_syncable.h"
35#include "content/public/browser/browser_thread.h"
36#include "content/public/browser/notification_service.h"
37#include "content/public/browser/render_process_host.h"
38#include "content/public/browser/render_view_host.h"
39#include "content/public/browser/url_data_source.h"
40#include "content/public/browser/web_contents.h"
41#include "content/public/browser/web_ui.h"
42#include "extensions/browser/extension_system.h"
43#include "grit/browser_resources.h"
44#include "ui/base/l10n/l10n_util.h"
45
46#if defined(ENABLE_THEMES)
47#include "chrome/browser/ui/webui/theme_handler.h"
48#endif
49
50#if defined(USE_ASH)
51#include "chrome/browser/ui/host_desktop.h"
52#endif
53
54using content::BrowserThread;
55using content::RenderViewHost;
56using content::WebUIController;
57
58namespace {
59
60// The amount of time there must be no painting for us to consider painting
61// finished.  Observed times are in the ~1200ms range on Windows.
62const int kTimeoutMs = 2000;
63
64// Strings sent to the page via jstemplates used to set the direction of the
65// HTML document based on locale.
66const char kRTLHtmlTextDirection[] = "rtl";
67const char kLTRHtmlTextDirection[] = "ltr";
68
69static base::LazyInstance<std::set<const WebUIController*> > g_live_new_tabs;
70
71const char* GetHtmlTextDirection(const base::string16& text) {
72  if (base::i18n::IsRTL() && base::i18n::StringContainsStrongRTLChars(text))
73    return kRTLHtmlTextDirection;
74  else
75    return kLTRHtmlTextDirection;
76}
77
78}  // namespace
79
80///////////////////////////////////////////////////////////////////////////////
81// NewTabUI
82
83NewTabUI::NewTabUI(content::WebUI* web_ui)
84    : WebUIController(web_ui),
85      WebContentsObserver(web_ui->GetWebContents()),
86      showing_sync_bubble_(false) {
87  g_live_new_tabs.Pointer()->insert(this);
88  web_ui->OverrideTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE));
89
90  // We count all link clicks as AUTO_BOOKMARK, so that site can be ranked more
91  // highly. Note this means we're including clicks on not only most visited
92  // thumbnails, but also clicks on recently bookmarked.
93  web_ui->SetLinkTransitionType(ui::PAGE_TRANSITION_AUTO_BOOKMARK);
94
95  Profile* profile = GetProfile();
96  if (!profile->IsOffTheRecord()) {
97    web_ui->AddMessageHandler(new browser_sync::ForeignSessionHandler());
98    web_ui->AddMessageHandler(new MetricsHandler());
99    web_ui->AddMessageHandler(new MostVisitedHandler());
100    web_ui->AddMessageHandler(new RecentlyClosedTabsHandler());
101    web_ui->AddMessageHandler(new FaviconWebUIHandler());
102    web_ui->AddMessageHandler(new NewTabPageHandler());
103    web_ui->AddMessageHandler(new CoreAppLauncherHandler());
104    if (NewTabUI::IsDiscoveryInNTPEnabled())
105      web_ui->AddMessageHandler(new SuggestionsHandler());
106    web_ui->AddMessageHandler(new NewTabPageSyncHandler());
107
108    ExtensionService* service =
109        extensions::ExtensionSystem::Get(profile)->extension_service();
110    // We might not have an ExtensionService (on ChromeOS when not logged in
111    // for example).
112    if (service)
113      web_ui->AddMessageHandler(new AppLauncherHandler(service));
114  }
115
116  if (NTPLoginHandler::ShouldShow(profile))
117    web_ui->AddMessageHandler(new NTPLoginHandler());
118
119#if defined(ENABLE_THEMES)
120  // The theme handler can require some CPU, so do it after hooking up the most
121  // visited handler. This allows the DB query for the new tab thumbs to happen
122  // earlier.
123  web_ui->AddMessageHandler(new ThemeHandler());
124#endif
125
126  scoped_ptr<NewTabHTMLSource> html_source(
127      new NewTabHTMLSource(profile->GetOriginalProfile()));
128
129  // These two resources should be loaded only if suggestions NTP is enabled.
130  html_source->AddResource("suggestions_page.css", "text/css",
131      NewTabUI::IsDiscoveryInNTPEnabled() ? IDR_SUGGESTIONS_PAGE_CSS : 0);
132  if (NewTabUI::IsDiscoveryInNTPEnabled()) {
133    html_source->AddResource("suggestions_page.js", "application/javascript",
134        IDR_SUGGESTIONS_PAGE_JS);
135  }
136  // content::URLDataSource assumes the ownership of the html_source.
137  content::URLDataSource::Add(profile, html_source.release());
138
139  pref_change_registrar_.Init(profile->GetPrefs());
140  pref_change_registrar_.Add(bookmarks::prefs::kShowBookmarkBar,
141                             base::Bind(&NewTabUI::OnShowBookmarkBarChanged,
142                                        base::Unretained(this)));
143}
144
145NewTabUI::~NewTabUI() {
146  g_live_new_tabs.Pointer()->erase(this);
147}
148
149// The timer callback.  If enough time has elapsed since the last paint
150// message, we say we're done painting; otherwise, we keep waiting.
151void NewTabUI::PaintTimeout() {
152  // The amount of time there must be no painting for us to consider painting
153  // finished.  Observed times are in the ~1200ms range on Windows.
154  base::TimeTicks now = base::TimeTicks::Now();
155  if ((now - last_paint_) >= base::TimeDelta::FromMilliseconds(kTimeoutMs)) {
156    // Painting has quieted down.  Log this as the full time to run.
157    base::TimeDelta load_time = last_paint_ - start_;
158    UMA_HISTOGRAM_TIMES("NewTabUI load", load_time);
159  } else {
160    // Not enough quiet time has elapsed.
161    // Some more paints must've occurred since we set the timeout.
162    // Wait some more.
163    timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kTimeoutMs), this,
164                 &NewTabUI::PaintTimeout);
165  }
166}
167
168void NewTabUI::StartTimingPaint(RenderViewHost* render_view_host) {
169  start_ = base::TimeTicks::Now();
170  last_paint_ = start_;
171
172  content::NotificationSource source =
173      content::Source<content::RenderWidgetHost>(render_view_host);
174  if (!registrar_.IsRegistered(this,
175          content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE,
176          source)) {
177    registrar_.Add(
178        this,
179        content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE,
180        source);
181  }
182
183  timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kTimeoutMs), this,
184               &NewTabUI::PaintTimeout);
185}
186
187void NewTabUI::RenderViewCreated(RenderViewHost* render_view_host) {
188  StartTimingPaint(render_view_host);
189}
190
191void NewTabUI::RenderViewReused(RenderViewHost* render_view_host) {
192  StartTimingPaint(render_view_host);
193}
194
195void NewTabUI::WasHidden() {
196  EmitNtpStatistics();
197}
198
199void NewTabUI::Observe(int type,
200                       const content::NotificationSource& source,
201                       const content::NotificationDetails& details) {
202  switch (type) {
203    case content::NOTIFICATION_RENDER_WIDGET_HOST_DID_UPDATE_BACKING_STORE: {
204      last_paint_ = base::TimeTicks::Now();
205      break;
206    }
207    default:
208      CHECK(false) << "Unexpected notification: " << type;
209  }
210}
211
212void NewTabUI::EmitNtpStatistics() {
213  NTPUserDataLogger::GetOrCreateFromWebContents(
214      web_contents())->EmitNtpStatistics();
215}
216
217void NewTabUI::OnShowBookmarkBarChanged() {
218  base::StringValue attached(
219      GetProfile()->GetPrefs()->GetBoolean(bookmarks::prefs::kShowBookmarkBar) ?
220          "true" : "false");
221  web_ui()->CallJavascriptFunction("ntp.setBookmarkBarAttached", attached);
222}
223
224// static
225void NewTabUI::RegisterProfilePrefs(
226    user_prefs::PrefRegistrySyncable* registry) {
227  CoreAppLauncherHandler::RegisterProfilePrefs(registry);
228  NewTabPageHandler::RegisterProfilePrefs(registry);
229  if (NewTabUI::IsDiscoveryInNTPEnabled())
230    SuggestionsHandler::RegisterProfilePrefs(registry);
231  MostVisitedHandler::RegisterProfilePrefs(registry);
232  browser_sync::ForeignSessionHandler::RegisterProfilePrefs(registry);
233}
234
235// static
236bool NewTabUI::ShouldShowApps() {
237// Ash shows apps in app list thus should not show apps page in NTP4.
238#if defined(USE_ASH)
239  return chrome::GetActiveDesktop() != chrome::HOST_DESKTOP_TYPE_ASH;
240#else
241  return true;
242#endif
243}
244
245// static
246bool NewTabUI::IsDiscoveryInNTPEnabled() {
247  // TODO(beaudoin): The flag was removed during a clean-up pass. We leave that
248  // here to easily enable it back when we will explore this option again.
249  return false;
250}
251
252// static
253void NewTabUI::SetUrlTitleAndDirection(base::DictionaryValue* dictionary,
254                                       const base::string16& title,
255                                       const GURL& gurl) {
256  dictionary->SetString("url", gurl.spec());
257
258  bool using_url_as_the_title = false;
259  base::string16 title_to_set(title);
260  if (title_to_set.empty()) {
261    using_url_as_the_title = true;
262    title_to_set = base::UTF8ToUTF16(gurl.spec());
263  }
264
265  // We set the "dir" attribute of the title, so that in RTL locales, a LTR
266  // title is rendered left-to-right and truncated from the right. For example,
267  // the title of http://msdn.microsoft.com/en-us/default.aspx is "MSDN:
268  // Microsoft developer network". In RTL locales, in the [New Tab] page, if
269  // the "dir" of this title is not specified, it takes Chrome UI's
270  // directionality. So the title will be truncated as "soft developer
271  // network". Setting the "dir" attribute as "ltr" renders the truncated title
272  // as "MSDN: Microsoft D...". As another example, the title of
273  // http://yahoo.com is "Yahoo!". In RTL locales, in the [New Tab] page, the
274  // title will be rendered as "!Yahoo" if its "dir" attribute is not set to
275  // "ltr".
276  std::string direction;
277  if (using_url_as_the_title)
278    direction = kLTRHtmlTextDirection;
279  else
280    direction = GetHtmlTextDirection(title);
281
282  dictionary->SetString("title", title_to_set);
283  dictionary->SetString("direction", direction);
284}
285
286// static
287void NewTabUI::SetFullNameAndDirection(const base::string16& full_name,
288                                       base::DictionaryValue* dictionary) {
289  dictionary->SetString("full_name", full_name);
290  dictionary->SetString("full_name_direction", GetHtmlTextDirection(full_name));
291}
292
293// static
294NewTabUI* NewTabUI::FromWebUIController(WebUIController* ui) {
295  if (!g_live_new_tabs.Pointer()->count(ui))
296    return NULL;
297  return static_cast<NewTabUI*>(ui);
298}
299
300Profile* NewTabUI::GetProfile() const {
301  return Profile::FromWebUI(web_ui());
302}
303
304///////////////////////////////////////////////////////////////////////////////
305// NewTabHTMLSource
306
307NewTabUI::NewTabHTMLSource::NewTabHTMLSource(Profile* profile)
308    : profile_(profile) {
309}
310
311std::string NewTabUI::NewTabHTMLSource::GetSource() const {
312  return chrome::kChromeUINewTabHost;
313}
314
315void NewTabUI::NewTabHTMLSource::StartDataRequest(
316    const std::string& path,
317    int render_process_id,
318    int render_frame_id,
319    const content::URLDataSource::GotDataCallback& callback) {
320  DCHECK_CURRENTLY_ON(BrowserThread::UI);
321
322  std::map<std::string, std::pair<std::string, int> >::iterator it =
323    resource_map_.find(path);
324  if (it != resource_map_.end()) {
325    scoped_refptr<base::RefCountedStaticMemory> resource_bytes(
326        it->second.second ?
327            ResourceBundle::GetSharedInstance().LoadDataResourceBytes(
328                it->second.second) :
329            new base::RefCountedStaticMemory);
330    callback.Run(resource_bytes.get());
331    return;
332  }
333
334  if (!path.empty() && path[0] != '#') {
335    // A path under new-tab was requested; it's likely a bad relative
336    // URL from the new tab page, but in any case it's an error.
337    NOTREACHED() << path << " should not have been requested on the NTP";
338    callback.Run(NULL);
339    return;
340  }
341
342  content::RenderProcessHost* render_host =
343      content::RenderProcessHost::FromID(render_process_id);
344  NTPResourceCache::WindowType win_type = NTPResourceCache::GetWindowType(
345      profile_, render_host);
346  scoped_refptr<base::RefCountedMemory> html_bytes(
347      NTPResourceCacheFactory::GetForProfile(profile_)->
348      GetNewTabHTML(win_type));
349
350  callback.Run(html_bytes.get());
351}
352
353std::string NewTabUI::NewTabHTMLSource::GetMimeType(const std::string& resource)
354    const {
355  std::map<std::string, std::pair<std::string, int> >::const_iterator it =
356      resource_map_.find(resource);
357  if (it != resource_map_.end())
358    return it->second.first;
359  return "text/html";
360}
361
362bool NewTabUI::NewTabHTMLSource::ShouldReplaceExistingSource() const {
363  return false;
364}
365
366bool NewTabUI::NewTabHTMLSource::ShouldAddContentSecurityPolicy() const {
367  return false;
368}
369
370void NewTabUI::NewTabHTMLSource::AddResource(const char* resource,
371                                             const char* mime_type,
372                                             int resource_id) {
373  DCHECK(resource);
374  DCHECK(mime_type);
375  resource_map_[std::string(resource)] =
376      std::make_pair(std::string(mime_type), resource_id);
377}
378
379NewTabUI::NewTabHTMLSource::~NewTabHTMLSource() {}
380