Add ENABLE_MEDIA_ROUTER define to builds other than Android and iOS.
[chromium-blink-merge.git] / chrome / browser / ui / app_list / start_page_service.cc
blob63811e9a0d893cda548141bdac8808e78f22ed34
1 // Copyright 2013 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.
5 #include "chrome/browser/ui/app_list/start_page_service.h"
7 #include <string>
9 #include "base/bind.h"
10 #include "base/command_line.h"
11 #include "base/json/json_string_value_serializer.h"
12 #include "base/memory/singleton.h"
13 #include "base/metrics/user_metrics.h"
14 #include "base/prefs/pref_service.h"
15 #include "base/strings/string_piece.h"
16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/chrome_notification_types.h"
18 #include "chrome/browser/media/media_stream_infobar_delegate.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/search/hotword_service.h"
21 #include "chrome/browser/search/hotword_service_factory.h"
22 #include "chrome/browser/search_engines/template_url_service_factory.h"
23 #include "chrome/browser/search_engines/ui_thread_search_terms_data.h"
24 #include "chrome/browser/ui/app_list/speech_auth_helper.h"
25 #include "chrome/browser/ui/app_list/speech_recognizer.h"
26 #include "chrome/browser/ui/app_list/start_page_observer.h"
27 #include "chrome/browser/ui/app_list/start_page_service_factory.h"
28 #include "chrome/browser/ui/browser_navigator.h"
29 #include "chrome/browser/ui/browser_tabstrip.h"
30 #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
31 #include "chrome/common/chrome_switches.h"
32 #include "chrome/common/pref_names.h"
33 #include "chrome/common/url_constants.h"
34 #include "components/search_engines/template_url_prepopulate_data.h"
35 #include "components/search_engines/template_url_service.h"
36 #include "components/ui/zoom/zoom_controller.h"
37 #include "content/public/browser/browser_thread.h"
38 #include "content/public/browser/notification_details.h"
39 #include "content/public/browser/notification_observer.h"
40 #include "content/public/browser/notification_registrar.h"
41 #include "content/public/browser/notification_service.h"
42 #include "content/public/browser/notification_source.h"
43 #include "content/public/browser/render_view_host.h"
44 #include "content/public/browser/render_widget_host_view.h"
45 #include "content/public/browser/speech_recognition_session_preamble.h"
46 #include "content/public/browser/web_contents.h"
47 #include "content/public/browser/web_contents_delegate.h"
48 #include "content/public/common/content_switches.h"
49 #include "extensions/browser/extension_system_provider.h"
50 #include "extensions/browser/extensions_browser_client.h"
51 #include "extensions/common/extension.h"
52 #include "net/base/load_flags.h"
53 #include "net/base/network_change_notifier.h"
54 #include "net/url_request/url_fetcher.h"
55 #include "ui/app_list/app_list_switches.h"
57 #if defined(OS_CHROMEOS)
58 #include "chromeos/audio/cras_audio_handler.h"
59 #endif
61 using base::RecordAction;
62 using base::UserMetricsAction;
64 namespace app_list {
66 namespace {
68 // Path to google.com's doodle JSON.
69 const char kDoodleJsonPath[] = "async/ddljson";
71 // Delay between checking for a new doodle when no doodle is found.
72 const int kDefaultDoodleRecheckDelayMinutes = 30;
74 // Delay before loading the start page WebContents on initialization.
75 const int kLoadContentsDelaySeconds = 5;
77 bool InSpeechRecognition(SpeechRecognitionState state) {
78 return state == SPEECH_RECOGNITION_RECOGNIZING ||
79 state == SPEECH_RECOGNITION_IN_SPEECH;
82 } // namespace
84 class StartPageService::ProfileDestroyObserver
85 : public content::NotificationObserver {
86 public:
87 explicit ProfileDestroyObserver(StartPageService* service)
88 : service_(service) {
89 if (service_->profile()->IsOffTheRecord()) {
90 // We need to be notified when the original profile gets destroyed as well
91 // as the OTR profile, because the original profile will be destroyed
92 // first, and a DCHECK at that time ensures that the OTR profile has 0
93 // hosts. See http://crbug.com/463419.
94 registrar_.Add(
95 this, chrome::NOTIFICATION_PROFILE_DESTROYED,
96 content::Source<Profile>(service_->profile()->GetOriginalProfile()));
98 registrar_.Add(this,
99 chrome::NOTIFICATION_PROFILE_DESTROYED,
100 content::Source<Profile>(service_->profile()));
102 ~ProfileDestroyObserver() override {}
104 private:
105 // content::NotificationObserver
106 void Observe(int type,
107 const content::NotificationSource& source,
108 const content::NotificationDetails& details) override {
109 DCHECK_EQ(chrome::NOTIFICATION_PROFILE_DESTROYED, type);
110 DCHECK(service_->profile()->IsSameProfile(
111 content::Source<Profile>(source).ptr()));
112 registrar_.RemoveAll();
113 service_->Shutdown();
116 StartPageService* service_; // Owner of this class.
117 content::NotificationRegistrar registrar_;
119 DISALLOW_COPY_AND_ASSIGN(ProfileDestroyObserver);
122 class StartPageService::StartPageWebContentsDelegate
123 : public content::WebContentsDelegate {
124 public:
125 explicit StartPageWebContentsDelegate(Profile* profile) : profile_(profile) {}
126 ~StartPageWebContentsDelegate() override {}
128 void RequestMediaAccessPermission(
129 content::WebContents* web_contents,
130 const content::MediaStreamRequest& request,
131 const content::MediaResponseCallback& callback) override {
132 if (MediaStreamInfoBarDelegate::Create(web_contents, request, callback))
133 NOTREACHED() << "Media stream not allowed for WebUI";
136 bool CheckMediaAccessPermission(content::WebContents* web_contents,
137 const GURL& security_origin,
138 content::MediaStreamType type) override {
139 return MediaCaptureDevicesDispatcher::GetInstance()
140 ->CheckMediaAccessPermission(web_contents, security_origin, type);
143 void AddNewContents(content::WebContents* source,
144 content::WebContents* new_contents,
145 WindowOpenDisposition disposition,
146 const gfx::Rect& initial_pos,
147 bool user_gesture,
148 bool* was_blocked) override {
149 chrome::ScopedTabbedBrowserDisplayer displayer(
150 profile_, chrome::GetActiveDesktop());
151 // Force all links to open in a new tab, even if they were trying to open a
152 // new window.
153 disposition =
154 disposition == NEW_BACKGROUND_TAB ? disposition : NEW_FOREGROUND_TAB;
155 chrome::AddWebContents(displayer.browser(),
156 nullptr,
157 new_contents,
158 disposition,
159 initial_pos,
160 user_gesture,
161 was_blocked);
164 content::WebContents* OpenURLFromTab(
165 content::WebContents* source,
166 const content::OpenURLParams& params) override {
167 // Force all links to open in a new tab, even if they were trying to open a
168 // window.
169 chrome::NavigateParams new_tab_params(
170 static_cast<Browser*>(nullptr), params.url, params.transition);
171 if (params.disposition == NEW_BACKGROUND_TAB) {
172 new_tab_params.disposition = NEW_BACKGROUND_TAB;
173 } else {
174 new_tab_params.disposition = NEW_FOREGROUND_TAB;
175 new_tab_params.window_action = chrome::NavigateParams::SHOW_WINDOW;
178 new_tab_params.initiating_profile = profile_;
179 chrome::Navigate(&new_tab_params);
181 return new_tab_params.target_contents;
184 private:
185 Profile* profile_;
187 DISALLOW_COPY_AND_ASSIGN(StartPageWebContentsDelegate);
190 #if defined(OS_CHROMEOS)
192 class StartPageService::AudioStatus
193 : public chromeos::CrasAudioHandler::AudioObserver {
194 public:
195 explicit AudioStatus(StartPageService* start_page_service)
196 : start_page_service_(start_page_service) {
197 chromeos::CrasAudioHandler::Get()->AddAudioObserver(this);
198 CheckAndUpdate();
201 ~AudioStatus() override {
202 chromeos::CrasAudioHandler::Get()->RemoveAudioObserver(this);
205 bool CanListen() {
206 chromeos::CrasAudioHandler* audio_handler =
207 chromeos::CrasAudioHandler::Get();
208 return (audio_handler->GetPrimaryActiveInputNode() != 0) &&
209 !audio_handler->IsInputMuted();
212 private:
213 void CheckAndUpdate() {
214 // TODO(mukai): If the system can listen, this should also restart the
215 // hotword recognition.
216 start_page_service_->OnMicrophoneChanged(CanListen());
219 // chromeos::CrasAudioHandler::AudioObserver:
220 void OnInputMuteChanged(bool /* mute_on */) override { CheckAndUpdate(); }
222 void OnActiveInputNodeChanged() override { CheckAndUpdate(); }
224 StartPageService* start_page_service_;
226 DISALLOW_COPY_AND_ASSIGN(AudioStatus);
229 #endif // OS_CHROMEOS
231 class StartPageService::NetworkChangeObserver
232 : public net::NetworkChangeNotifier::NetworkChangeObserver {
233 public:
234 explicit NetworkChangeObserver(StartPageService* start_page_service)
235 : start_page_service_(start_page_service) {
236 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
237 // NOTE: This is used to detect network connectivity changes. However, what
238 // we really want is internet connectivity changes because voice recognition
239 // needs to talk to a web service. However, this information isn't
240 // available, so network changes are the best we can do.
241 net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
243 last_type_ = net::NetworkChangeNotifier::GetConnectionType();
244 // Handle the case where we're started with no network available.
245 if (last_type_ == net::NetworkChangeNotifier::CONNECTION_NONE)
246 start_page_service_->OnNetworkChanged(false);
249 ~NetworkChangeObserver() override {
250 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
251 net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
254 private:
255 void OnNetworkChanged(
256 net::NetworkChangeNotifier::ConnectionType type) override {
257 // Threading note: NetworkChangeNotifier's contract is that observers are
258 // called on the same thread that they're registered. In this case, it
259 // should always be the UI thread.
260 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
261 if (type == net::NetworkChangeNotifier::CONNECTION_NONE) {
262 start_page_service_->OnNetworkChanged(false);
263 } else if (last_type_ == net::NetworkChangeNotifier::CONNECTION_NONE &&
264 type != net::NetworkChangeNotifier::CONNECTION_NONE) {
265 start_page_service_->OnNetworkChanged(true);
267 last_type_ = type;
270 StartPageService* start_page_service_;
271 net::NetworkChangeNotifier::ConnectionType last_type_;
273 DISALLOW_COPY_AND_ASSIGN(NetworkChangeObserver);
276 // static
277 StartPageService* StartPageService::Get(Profile* profile) {
278 return StartPageServiceFactory::GetForProfile(profile);
281 StartPageService::StartPageService(Profile* profile)
282 : profile_(profile),
283 profile_destroy_observer_(new ProfileDestroyObserver(this)),
284 state_(app_list::SPEECH_RECOGNITION_READY),
285 speech_button_toggled_manually_(false),
286 speech_result_obtained_(false),
287 webui_finished_loading_(false),
288 speech_auth_helper_(new SpeechAuthHelper(profile, &clock_)),
289 network_available_(true),
290 microphone_available_(true),
291 search_engine_is_google_(false),
292 weak_factory_(this) {
293 if (switches::IsExperimentalAppListEnabled()) {
294 TemplateURLService* template_url_service =
295 TemplateURLServiceFactory::GetForProfile(profile_);
296 const TemplateURL* default_provider =
297 template_url_service->GetDefaultSearchProvider();
298 search_engine_is_google_ =
299 TemplateURLPrepopulateData::GetEngineType(
300 *default_provider, template_url_service->search_terms_data()) ==
301 SEARCH_ENGINE_GOOGLE;
304 network_change_observer_.reset(new NetworkChangeObserver(this));
307 StartPageService::~StartPageService() {
310 void StartPageService::AddObserver(StartPageObserver* observer) {
311 observers_.AddObserver(observer);
314 void StartPageService::RemoveObserver(StartPageObserver* observer) {
315 observers_.RemoveObserver(observer);
318 void StartPageService::OnMicrophoneChanged(bool available) {
319 microphone_available_ = available;
320 UpdateRecognitionState();
323 void StartPageService::OnNetworkChanged(bool available) {
324 network_available_ = available;
325 UpdateRecognitionState();
328 void StartPageService::UpdateRecognitionState() {
329 if (ShouldEnableSpeechRecognition()) {
330 if (state_ == SPEECH_RECOGNITION_OFF ||
331 state_ == SPEECH_RECOGNITION_NETWORK_ERROR)
332 OnSpeechRecognitionStateChanged(SPEECH_RECOGNITION_READY);
333 } else {
334 OnSpeechRecognitionStateChanged(network_available_ ? SPEECH_RECOGNITION_OFF
335 : SPEECH_RECOGNITION_NETWORK_ERROR);
339 void StartPageService::Init() {
340 // Do not load the start page web contents in tests because many tests assume
341 // no WebContents exist except the ones they make.
342 if (switches::IsExperimentalAppListEnabled() &&
343 !base::CommandLine::ForCurrentProcess()->HasSwitch(
344 ::switches::kTestType)) {
345 content::BrowserThread::PostDelayedTask(
346 content::BrowserThread::UI, FROM_HERE,
347 base::Bind(&StartPageService::LoadContentsIfNeeded,
348 weak_factory_.GetWeakPtr()),
349 base::TimeDelta::FromSeconds(kLoadContentsDelaySeconds));
353 void StartPageService::LoadContentsIfNeeded() {
354 if (!contents_)
355 LoadContents();
358 bool StartPageService::ShouldEnableSpeechRecognition() const {
359 return microphone_available_ && network_available_;
362 void StartPageService::AppListShown() {
363 if (!contents_) {
364 LoadContents();
365 } else if (contents_->IsCrashed()) {
366 LoadStartPageURL();
367 } else if (contents_->GetWebUI()) {
368 contents_->GetWebUI()->CallJavascriptFunction(
369 "appList.startPage.onAppListShown");
372 #if defined(OS_CHROMEOS)
373 audio_status_.reset(new AudioStatus(this));
374 #endif
377 void StartPageService::AppListHidden() {
378 if (!app_list::switches::IsExperimentalAppListEnabled())
379 UnloadContents();
381 if (speech_recognizer_) {
382 speech_recognizer_->Stop();
383 speech_recognizer_.reset();
385 // When the SpeechRecognizer is destroyed above, we get stuck in the current
386 // speech state instead of being reset into the READY state. Reset the
387 // speech state explicitly so that speech works when the launcher is opened
388 // again.
389 OnSpeechRecognitionStateChanged(SPEECH_RECOGNITION_READY);
392 #if defined(OS_CHROMEOS)
393 audio_status_.reset();
394 #endif
397 void StartPageService::ToggleSpeechRecognition(
398 const scoped_refptr<content::SpeechRecognitionSessionPreamble>& preamble) {
399 DCHECK(contents_);
400 speech_button_toggled_manually_ = true;
402 if (!speech_recognizer_) {
403 std::string profile_locale;
404 #if defined(OS_CHROMEOS)
405 profile_locale = profile_->GetPrefs()->GetString(
406 prefs::kApplicationLocale);
407 #endif
408 if (profile_locale.empty())
409 profile_locale = g_browser_process->GetApplicationLocale();
411 speech_recognizer_.reset(
412 new SpeechRecognizer(weak_factory_.GetWeakPtr(),
413 profile_->GetRequestContext(),
414 profile_locale));
417 speech_recognizer_->Start(preamble);
420 bool StartPageService::HotwordEnabled() {
421 // Voice input for the launcher is unsupported on non-ChromeOS platforms.
422 // TODO(amistry): Make speech input, and hotwording, work on non-ChromeOS.
423 #if defined(OS_CHROMEOS)
424 HotwordService* service = HotwordServiceFactory::GetForProfile(profile_);
425 return state_ != SPEECH_RECOGNITION_OFF &&
426 service &&
427 (service->IsSometimesOnEnabled() || service->IsAlwaysOnEnabled()) &&
428 service->IsServiceAvailable();
429 #else
430 return false;
431 #endif
434 content::WebContents* StartPageService::GetStartPageContents() {
435 return app_list::switches::IsExperimentalAppListEnabled() ? contents_.get()
436 : NULL;
439 content::WebContents* StartPageService::GetSpeechRecognitionContents() {
440 if (app_list::switches::IsVoiceSearchEnabled()) {
441 if (!contents_)
442 LoadContents();
443 return contents_.get();
445 return NULL;
448 void StartPageService::OnSpeechResult(
449 const base::string16& query, bool is_final) {
450 if (is_final) {
451 speech_result_obtained_ = true;
452 RecordAction(UserMetricsAction("AppList_SearchedBySpeech"));
454 FOR_EACH_OBSERVER(StartPageObserver,
455 observers_,
456 OnSpeechResult(query, is_final));
459 void StartPageService::OnSpeechSoundLevelChanged(int16_t level) {
460 FOR_EACH_OBSERVER(StartPageObserver,
461 observers_,
462 OnSpeechSoundLevelChanged(level));
465 void StartPageService::OnSpeechRecognitionStateChanged(
466 SpeechRecognitionState new_state) {
467 #if defined(OS_CHROMEOS)
468 // Sometimes this can be called even though there are no audio input devices.
469 if (audio_status_ && !audio_status_->CanListen())
470 new_state = SPEECH_RECOGNITION_OFF;
471 #endif
472 if (!microphone_available_)
473 new_state = SPEECH_RECOGNITION_OFF;
474 if (!network_available_)
475 new_state = SPEECH_RECOGNITION_NETWORK_ERROR;
477 if (state_ == new_state)
478 return;
480 if ((new_state == SPEECH_RECOGNITION_READY ||
481 new_state == SPEECH_RECOGNITION_OFF ||
482 new_state == SPEECH_RECOGNITION_NETWORK_ERROR) &&
483 speech_recognizer_) {
484 speech_recognizer_->Stop();
487 if (!InSpeechRecognition(state_) && InSpeechRecognition(new_state)) {
488 if (!speech_button_toggled_manually_ &&
489 state_ == SPEECH_RECOGNITION_HOTWORD_LISTENING) {
490 RecordAction(UserMetricsAction("AppList_HotwordRecognized"));
491 } else {
492 RecordAction(UserMetricsAction("AppList_VoiceSearchStartedManually"));
494 } else if (InSpeechRecognition(state_) && !InSpeechRecognition(new_state) &&
495 !speech_result_obtained_) {
496 RecordAction(UserMetricsAction("AppList_VoiceSearchCanceled"));
498 speech_button_toggled_manually_ = false;
499 speech_result_obtained_ = false;
500 state_ = new_state;
501 FOR_EACH_OBSERVER(StartPageObserver,
502 observers_,
503 OnSpeechRecognitionStateChanged(new_state));
506 void StartPageService::GetSpeechAuthParameters(std::string* auth_scope,
507 std::string* auth_token) {
508 HotwordService* service = HotwordServiceFactory::GetForProfile(profile_);
509 if (service &&
510 service->IsOptedIntoAudioLogging() &&
511 service->IsAlwaysOnEnabled() &&
512 !speech_auth_helper_->GetToken().empty()) {
513 *auth_scope = speech_auth_helper_->GetScope();
514 *auth_token = speech_auth_helper_->GetToken();
518 void StartPageService::Shutdown() {
519 UnloadContents();
520 #if defined(OS_CHROMEOS)
521 audio_status_.reset();
522 #endif
524 speech_auth_helper_.reset();
525 network_change_observer_.reset();
528 void StartPageService::DidNavigateMainFrame(
529 const content::LoadCommittedDetails& /*details*/,
530 const content::FrameNavigateParams& /*params*/) {
531 // Set the zoom level in DidNavigateMainFrame, as this is the earliest point
532 // at which it can be done and not be affected by the ZoomController's
533 // DidNavigateMainFrame handler.
535 // Use a temporary zoom level for this web contents (aka isolated zoom
536 // mode) so changes to its zoom aren't reflected in any preferences.
537 ui_zoom::ZoomController::FromWebContents(contents_.get())
538 ->SetZoomMode(ui_zoom::ZoomController::ZOOM_MODE_ISOLATED);
539 // Set to have a zoom level of 0, which corresponds to 100%, so the
540 // contents aren't affected by the browser's default zoom level.
541 ui_zoom::ZoomController::FromWebContents(contents_.get())->SetZoomLevel(0);
544 void StartPageService::WebUILoaded() {
545 // There's a race condition between the WebUI loading, and calling its JS
546 // functions. Specifically, calling LoadContents() doesn't mean that the page
547 // has loaded, but several code paths make this assumption. This function
548 // allows us to defer calling JS functions until after the page has finished
549 // loading.
550 webui_finished_loading_ = true;
551 for (const auto& cb : pending_webui_callbacks_)
552 cb.Run();
553 pending_webui_callbacks_.clear();
555 FetchDoodleJson();
558 void StartPageService::LoadContents() {
559 contents_.reset(content::WebContents::Create(
560 content::WebContents::CreateParams(profile_)));
561 contents_delegate_.reset(new StartPageWebContentsDelegate(profile_));
562 contents_->SetDelegate(contents_delegate_.get());
564 // The ZoomController needs to be created before the web contents is observed
565 // by this object. Otherwise it will react to DidNavigateMainFrame after this
566 // object does, resetting the zoom mode in the process.
567 ui_zoom::ZoomController::CreateForWebContents(contents_.get());
568 Observe(contents_.get());
570 LoadStartPageURL();
573 void StartPageService::UnloadContents() {
574 contents_.reset();
575 webui_finished_loading_ = false;
578 void StartPageService::LoadStartPageURL() {
579 contents_->GetController().LoadURL(
580 GURL(chrome::kChromeUIAppListStartPageURL),
581 content::Referrer(),
582 ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
583 std::string());
585 contents_->GetRenderViewHost()->GetView()->SetBackgroundColor(
586 SK_ColorTRANSPARENT);
589 void StartPageService::FetchDoodleJson() {
590 if (!search_engine_is_google_)
591 return;
593 GURL::Replacements replacements;
594 replacements.SetPathStr(kDoodleJsonPath);
596 GURL google_base_url(UIThreadSearchTermsData(profile_).GoogleBaseURLValue());
597 GURL doodle_url = google_base_url.ReplaceComponents(replacements);
598 doodle_fetcher_.reset(
599 net::URLFetcher::Create(0, doodle_url, net::URLFetcher::GET, this));
600 doodle_fetcher_->SetRequestContext(profile_->GetRequestContext());
601 doodle_fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES);
602 doodle_fetcher_->Start();
605 void StartPageService::OnURLFetchComplete(const net::URLFetcher* source) {
606 std::string json_data;
607 source->GetResponseAsString(&json_data);
609 // Remove XSSI guard for JSON parsing.
610 size_t json_start_index = json_data.find("{");
611 base::StringPiece json_data_substr(json_data);
612 if (json_start_index != std::string::npos)
613 json_data_substr.remove_prefix(json_start_index);
615 JSONStringValueDeserializer deserializer(json_data_substr);
616 deserializer.set_allow_trailing_comma(true);
617 int error_code = 0;
618 scoped_ptr<base::Value> doodle_json(
619 deserializer.Deserialize(&error_code, nullptr));
621 base::TimeDelta recheck_delay =
622 base::TimeDelta::FromMinutes(kDefaultDoodleRecheckDelayMinutes);
624 if (error_code == 0) {
625 base::DictionaryValue* doodle_dictionary = nullptr;
626 // Use the supplied TTL as the recheck delay if available.
627 if (doodle_json->GetAsDictionary(&doodle_dictionary)) {
628 int time_to_live = 0;
629 if (doodle_dictionary->GetInteger("ddljson.time_to_live_ms",
630 &time_to_live)) {
631 recheck_delay = base::TimeDelta::FromMilliseconds(time_to_live);
635 if (contents_ && contents_->GetWebUI()) {
636 contents_->GetWebUI()->CallJavascriptFunction(
637 "appList.startPage.onAppListDoodleUpdated", *doodle_json,
638 base::StringValue(
639 UIThreadSearchTermsData(profile_).GoogleBaseURLValue()));
643 // Check for a new doodle.
644 content::BrowserThread::PostDelayedTask(
645 content::BrowserThread::UI, FROM_HERE,
646 base::Bind(&StartPageService::FetchDoodleJson,
647 weak_factory_.GetWeakPtr()),
648 recheck_delay);
651 } // namespace app_list