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"
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"
61 using base::RecordAction
;
62 using base::UserMetricsAction
;
68 // Path to google.com's doodle JSON.
69 const char kDoodleJsonPath
[] = "async/ddljson";
71 // Maximum delay between checking for a new doodle when the doodle cannot be
72 // retrieved. This is also used as the delay once a doodle is retrieved.
73 const int kMaximumRecheckDelayMs
= 1000 * 60 * 30; // 30 minutes.
75 // Delay before loading the start page WebContents on initialization.
76 const int kLoadContentsDelaySeconds
= 5;
78 const net::BackoffEntry::Policy kDoodleBackoffPolicy
= {
79 // Number of initial errors (in sequence) to ignore before applying
80 // exponential back-off rules.
83 // Initial delay for exponential back-off in ms.
86 // Factor by which the waiting time will be multiplied.
89 // Fuzzing percentage. ex: 10% will spread requests randomly
90 // between 90%-100% of the calculated time.
93 // Maximum amount of time we are willing to delay our request in ms.
94 kMaximumRecheckDelayMs
,
96 // Time to keep an entry from being discarded even when it
97 // has no significant state, -1 to never discard.
100 // Don't use initial delay unless the last request was an error.
104 bool InSpeechRecognition(SpeechRecognitionState state
) {
105 return state
== SPEECH_RECOGNITION_RECOGNIZING
||
106 state
== SPEECH_RECOGNITION_IN_SPEECH
;
111 class StartPageService::ProfileDestroyObserver
112 : public content::NotificationObserver
{
114 explicit ProfileDestroyObserver(StartPageService
* service
)
115 : service_(service
) {
116 if (service_
->profile()->IsOffTheRecord()) {
117 // We need to be notified when the original profile gets destroyed as well
118 // as the OTR profile, because the original profile will be destroyed
119 // first, and a DCHECK at that time ensures that the OTR profile has 0
120 // hosts. See http://crbug.com/463419.
122 this, chrome::NOTIFICATION_PROFILE_DESTROYED
,
123 content::Source
<Profile
>(service_
->profile()->GetOriginalProfile()));
126 chrome::NOTIFICATION_PROFILE_DESTROYED
,
127 content::Source
<Profile
>(service_
->profile()));
129 ~ProfileDestroyObserver() override
{}
132 // content::NotificationObserver
133 void Observe(int type
,
134 const content::NotificationSource
& source
,
135 const content::NotificationDetails
& details
) override
{
136 DCHECK_EQ(chrome::NOTIFICATION_PROFILE_DESTROYED
, type
);
137 DCHECK(service_
->profile()->IsSameProfile(
138 content::Source
<Profile
>(source
).ptr()));
139 registrar_
.RemoveAll();
140 service_
->Shutdown();
143 StartPageService
* service_
; // Owner of this class.
144 content::NotificationRegistrar registrar_
;
146 DISALLOW_COPY_AND_ASSIGN(ProfileDestroyObserver
);
149 class StartPageService::StartPageWebContentsDelegate
150 : public content::WebContentsDelegate
{
152 explicit StartPageWebContentsDelegate(Profile
* profile
) : profile_(profile
) {}
153 ~StartPageWebContentsDelegate() override
{}
155 void RequestMediaAccessPermission(
156 content::WebContents
* web_contents
,
157 const content::MediaStreamRequest
& request
,
158 const content::MediaResponseCallback
& callback
) override
{
159 if (MediaStreamInfoBarDelegate::Create(web_contents
, request
, callback
))
160 NOTREACHED() << "Media stream not allowed for WebUI";
163 bool CheckMediaAccessPermission(content::WebContents
* web_contents
,
164 const GURL
& security_origin
,
165 content::MediaStreamType type
) override
{
166 return MediaCaptureDevicesDispatcher::GetInstance()
167 ->CheckMediaAccessPermission(web_contents
, security_origin
, type
);
170 void AddNewContents(content::WebContents
* source
,
171 content::WebContents
* new_contents
,
172 WindowOpenDisposition disposition
,
173 const gfx::Rect
& initial_pos
,
175 bool* was_blocked
) override
{
176 chrome::ScopedTabbedBrowserDisplayer
displayer(
177 profile_
, chrome::GetActiveDesktop());
178 // Force all links to open in a new tab, even if they were trying to open a
181 disposition
== NEW_BACKGROUND_TAB
? disposition
: NEW_FOREGROUND_TAB
;
182 chrome::AddWebContents(displayer
.browser(),
191 content::WebContents
* OpenURLFromTab(
192 content::WebContents
* source
,
193 const content::OpenURLParams
& params
) override
{
194 // Force all links to open in a new tab, even if they were trying to open a
196 chrome::NavigateParams
new_tab_params(
197 static_cast<Browser
*>(nullptr), params
.url
, params
.transition
);
198 if (params
.disposition
== NEW_BACKGROUND_TAB
) {
199 new_tab_params
.disposition
= NEW_BACKGROUND_TAB
;
201 new_tab_params
.disposition
= NEW_FOREGROUND_TAB
;
202 new_tab_params
.window_action
= chrome::NavigateParams::SHOW_WINDOW
;
205 new_tab_params
.initiating_profile
= profile_
;
206 chrome::Navigate(&new_tab_params
);
208 return new_tab_params
.target_contents
;
211 bool PreHandleGestureEvent(content::WebContents
* /*source*/,
212 const blink::WebGestureEvent
& event
) override
{
213 // Disable pinch zooming on the start page web contents.
214 return event
.type
== blink::WebGestureEvent::GesturePinchBegin
||
215 event
.type
== blink::WebGestureEvent::GesturePinchUpdate
||
216 event
.type
== blink::WebGestureEvent::GesturePinchEnd
;
223 DISALLOW_COPY_AND_ASSIGN(StartPageWebContentsDelegate
);
226 #if defined(OS_CHROMEOS)
228 class StartPageService::AudioStatus
229 : public chromeos::CrasAudioHandler::AudioObserver
{
231 explicit AudioStatus(StartPageService
* start_page_service
)
232 : start_page_service_(start_page_service
) {
233 chromeos::CrasAudioHandler::Get()->AddAudioObserver(this);
237 ~AudioStatus() override
{
238 chromeos::CrasAudioHandler::Get()->RemoveAudioObserver(this);
242 chromeos::CrasAudioHandler
* audio_handler
=
243 chromeos::CrasAudioHandler::Get();
244 return (audio_handler
->GetPrimaryActiveInputNode() != 0) &&
245 !audio_handler
->IsInputMuted();
249 void CheckAndUpdate() {
250 // TODO(mukai): If the system can listen, this should also restart the
251 // hotword recognition.
252 start_page_service_
->OnMicrophoneChanged(CanListen());
255 // chromeos::CrasAudioHandler::AudioObserver:
256 void OnInputMuteChanged(bool /* mute_on */) override
{ CheckAndUpdate(); }
258 void OnActiveInputNodeChanged() override
{ CheckAndUpdate(); }
260 StartPageService
* start_page_service_
;
262 DISALLOW_COPY_AND_ASSIGN(AudioStatus
);
265 #endif // OS_CHROMEOS
267 class StartPageService::NetworkChangeObserver
268 : public net::NetworkChangeNotifier::NetworkChangeObserver
{
270 explicit NetworkChangeObserver(StartPageService
* start_page_service
)
271 : start_page_service_(start_page_service
) {
272 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
273 // NOTE: This is used to detect network connectivity changes. However, what
274 // we really want is internet connectivity changes because voice recognition
275 // needs to talk to a web service. However, this information isn't
276 // available, so network changes are the best we can do.
277 net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
279 last_type_
= net::NetworkChangeNotifier::GetConnectionType();
280 // Handle the case where we're started with no network available.
281 if (last_type_
== net::NetworkChangeNotifier::CONNECTION_NONE
)
282 start_page_service_
->OnNetworkChanged(false);
285 ~NetworkChangeObserver() override
{
286 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
287 net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
291 void OnNetworkChanged(
292 net::NetworkChangeNotifier::ConnectionType type
) override
{
293 // Threading note: NetworkChangeNotifier's contract is that observers are
294 // called on the same thread that they're registered. In this case, it
295 // should always be the UI thread.
296 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
297 if (type
== net::NetworkChangeNotifier::CONNECTION_NONE
) {
298 start_page_service_
->OnNetworkChanged(false);
299 } else if (last_type_
== net::NetworkChangeNotifier::CONNECTION_NONE
&&
300 type
!= net::NetworkChangeNotifier::CONNECTION_NONE
) {
301 start_page_service_
->OnNetworkChanged(true);
306 StartPageService
* start_page_service_
;
307 net::NetworkChangeNotifier::ConnectionType last_type_
;
309 DISALLOW_COPY_AND_ASSIGN(NetworkChangeObserver
);
313 StartPageService
* StartPageService::Get(Profile
* profile
) {
314 return StartPageServiceFactory::GetForProfile(profile
);
317 StartPageService::StartPageService(Profile
* profile
)
319 profile_destroy_observer_(new ProfileDestroyObserver(this)),
320 state_(app_list::SPEECH_RECOGNITION_READY
),
321 speech_button_toggled_manually_(false),
322 speech_result_obtained_(false),
323 webui_finished_loading_(false),
324 speech_auth_helper_(new SpeechAuthHelper(profile
, &clock_
)),
325 network_available_(true),
326 microphone_available_(true),
327 search_engine_is_google_(false),
328 backoff_entry_(&kDoodleBackoffPolicy
),
329 weak_factory_(this) {
330 if (switches::IsExperimentalAppListEnabled()) {
331 TemplateURLService
* template_url_service
=
332 TemplateURLServiceFactory::GetForProfile(profile_
);
333 const TemplateURL
* default_provider
=
334 template_url_service
->GetDefaultSearchProvider();
335 search_engine_is_google_
=
336 TemplateURLPrepopulateData::GetEngineType(
337 *default_provider
, template_url_service
->search_terms_data()) ==
338 SEARCH_ENGINE_GOOGLE
;
341 network_change_observer_
.reset(new NetworkChangeObserver(this));
344 StartPageService::~StartPageService() {
347 void StartPageService::AddObserver(StartPageObserver
* observer
) {
348 observers_
.AddObserver(observer
);
351 void StartPageService::RemoveObserver(StartPageObserver
* observer
) {
352 observers_
.RemoveObserver(observer
);
355 void StartPageService::OnMicrophoneChanged(bool available
) {
356 microphone_available_
= available
;
357 UpdateRecognitionState();
360 void StartPageService::OnNetworkChanged(bool available
) {
361 network_available_
= available
;
362 UpdateRecognitionState();
365 void StartPageService::UpdateRecognitionState() {
366 if (ShouldEnableSpeechRecognition()) {
367 if (state_
== SPEECH_RECOGNITION_OFF
||
368 state_
== SPEECH_RECOGNITION_NETWORK_ERROR
)
369 OnSpeechRecognitionStateChanged(SPEECH_RECOGNITION_READY
);
371 OnSpeechRecognitionStateChanged(network_available_
? SPEECH_RECOGNITION_OFF
372 : SPEECH_RECOGNITION_NETWORK_ERROR
);
376 void StartPageService::Init() {
377 // Do not load the start page web contents in tests because many tests assume
378 // no WebContents exist except the ones they make.
379 if (switches::IsExperimentalAppListEnabled() &&
380 !base::CommandLine::ForCurrentProcess()->HasSwitch(
381 ::switches::kTestType
)) {
382 content::BrowserThread::PostDelayedTask(
383 content::BrowserThread::UI
, FROM_HERE
,
384 base::Bind(&StartPageService::LoadContentsIfNeeded
,
385 weak_factory_
.GetWeakPtr()),
386 base::TimeDelta::FromSeconds(kLoadContentsDelaySeconds
));
390 void StartPageService::LoadContentsIfNeeded() {
395 bool StartPageService::ShouldEnableSpeechRecognition() const {
396 return microphone_available_
&& network_available_
;
399 void StartPageService::AppListShown() {
402 } else if (contents_
->IsCrashed()) {
404 } else if (contents_
->GetWebUI()) {
405 contents_
->GetWebUI()->CallJavascriptFunction(
406 "appList.startPage.onAppListShown");
409 #if defined(OS_CHROMEOS)
410 audio_status_
.reset(new AudioStatus(this));
414 void StartPageService::AppListHidden() {
415 if (!app_list::switches::IsExperimentalAppListEnabled())
418 if (speech_recognizer_
) {
419 speech_recognizer_
->Stop();
420 speech_recognizer_
.reset();
422 // When the SpeechRecognizer is destroyed above, we get stuck in the current
423 // speech state instead of being reset into the READY state. Reset the
424 // speech state explicitly so that speech works when the launcher is opened
426 OnSpeechRecognitionStateChanged(SPEECH_RECOGNITION_READY
);
429 #if defined(OS_CHROMEOS)
430 audio_status_
.reset();
434 void StartPageService::ToggleSpeechRecognition(
435 const scoped_refptr
<content::SpeechRecognitionSessionPreamble
>& preamble
) {
437 speech_button_toggled_manually_
= true;
439 if (!speech_recognizer_
) {
440 std::string profile_locale
;
441 #if defined(OS_CHROMEOS)
442 profile_locale
= profile_
->GetPrefs()->GetString(
443 prefs::kApplicationLocale
);
445 if (profile_locale
.empty())
446 profile_locale
= g_browser_process
->GetApplicationLocale();
448 speech_recognizer_
.reset(
449 new SpeechRecognizer(weak_factory_
.GetWeakPtr(),
450 profile_
->GetRequestContext(),
454 speech_recognizer_
->Start(preamble
);
457 bool StartPageService::HotwordEnabled() {
458 // Voice input for the launcher is unsupported on non-ChromeOS platforms.
459 // TODO(amistry): Make speech input, and hotwording, work on non-ChromeOS.
460 #if defined(OS_CHROMEOS)
461 HotwordService
* service
= HotwordServiceFactory::GetForProfile(profile_
);
462 return state_
!= SPEECH_RECOGNITION_OFF
&&
464 (service
->IsSometimesOnEnabled() || service
->IsAlwaysOnEnabled()) &&
465 service
->IsServiceAvailable();
471 content::WebContents
* StartPageService::GetStartPageContents() {
472 return app_list::switches::IsExperimentalAppListEnabled() ? contents_
.get()
476 content::WebContents
* StartPageService::GetSpeechRecognitionContents() {
477 if (app_list::switches::IsVoiceSearchEnabled()) {
480 return contents_
.get();
485 void StartPageService::OnSpeechResult(
486 const base::string16
& query
, bool is_final
) {
488 speech_result_obtained_
= true;
489 RecordAction(UserMetricsAction("AppList_SearchedBySpeech"));
491 FOR_EACH_OBSERVER(StartPageObserver
,
493 OnSpeechResult(query
, is_final
));
496 void StartPageService::OnSpeechSoundLevelChanged(int16_t level
) {
497 FOR_EACH_OBSERVER(StartPageObserver
,
499 OnSpeechSoundLevelChanged(level
));
502 void StartPageService::OnSpeechRecognitionStateChanged(
503 SpeechRecognitionState new_state
) {
504 #if defined(OS_CHROMEOS)
505 // Sometimes this can be called even though there are no audio input devices.
506 if (audio_status_
&& !audio_status_
->CanListen())
507 new_state
= SPEECH_RECOGNITION_OFF
;
509 if (!microphone_available_
)
510 new_state
= SPEECH_RECOGNITION_OFF
;
511 if (!network_available_
)
512 new_state
= SPEECH_RECOGNITION_NETWORK_ERROR
;
514 if (state_
== new_state
)
517 if ((new_state
== SPEECH_RECOGNITION_READY
||
518 new_state
== SPEECH_RECOGNITION_OFF
||
519 new_state
== SPEECH_RECOGNITION_NETWORK_ERROR
) &&
520 speech_recognizer_
) {
521 speech_recognizer_
->Stop();
524 if (!InSpeechRecognition(state_
) && InSpeechRecognition(new_state
)) {
525 if (!speech_button_toggled_manually_
&&
526 state_
== SPEECH_RECOGNITION_HOTWORD_LISTENING
) {
527 RecordAction(UserMetricsAction("AppList_HotwordRecognized"));
529 RecordAction(UserMetricsAction("AppList_VoiceSearchStartedManually"));
531 } else if (InSpeechRecognition(state_
) && !InSpeechRecognition(new_state
) &&
532 !speech_result_obtained_
) {
533 RecordAction(UserMetricsAction("AppList_VoiceSearchCanceled"));
535 speech_button_toggled_manually_
= false;
536 speech_result_obtained_
= false;
538 FOR_EACH_OBSERVER(StartPageObserver
,
540 OnSpeechRecognitionStateChanged(new_state
));
543 void StartPageService::GetSpeechAuthParameters(std::string
* auth_scope
,
544 std::string
* auth_token
) {
545 HotwordService
* service
= HotwordServiceFactory::GetForProfile(profile_
);
547 service
->IsOptedIntoAudioLogging() &&
548 service
->IsAlwaysOnEnabled() &&
549 !speech_auth_helper_
->GetToken().empty()) {
550 *auth_scope
= speech_auth_helper_
->GetScope();
551 *auth_token
= speech_auth_helper_
->GetToken();
555 void StartPageService::Shutdown() {
557 #if defined(OS_CHROMEOS)
558 audio_status_
.reset();
561 speech_auth_helper_
.reset();
562 network_change_observer_
.reset();
565 void StartPageService::DidNavigateMainFrame(
566 const content::LoadCommittedDetails
& /*details*/,
567 const content::FrameNavigateParams
& /*params*/) {
568 // Set the zoom level in DidNavigateMainFrame, as this is the earliest point
569 // at which it can be done and not be affected by the ZoomController's
570 // DidNavigateMainFrame handler.
572 // Use a temporary zoom level for this web contents (aka isolated zoom
573 // mode) so changes to its zoom aren't reflected in any preferences.
574 ui_zoom::ZoomController::FromWebContents(contents_
.get())
575 ->SetZoomMode(ui_zoom::ZoomController::ZOOM_MODE_ISOLATED
);
576 // Set to have a zoom level of 0, which corresponds to 100%, so the
577 // contents aren't affected by the browser's default zoom level.
578 ui_zoom::ZoomController::FromWebContents(contents_
.get())->SetZoomLevel(0);
581 void StartPageService::WebUILoaded() {
582 // There's a race condition between the WebUI loading, and calling its JS
583 // functions. Specifically, calling LoadContents() doesn't mean that the page
584 // has loaded, but several code paths make this assumption. This function
585 // allows us to defer calling JS functions until after the page has finished
587 webui_finished_loading_
= true;
588 for (const auto& cb
: pending_webui_callbacks_
)
590 pending_webui_callbacks_
.clear();
595 void StartPageService::LoadContents() {
596 contents_
.reset(content::WebContents::Create(
597 content::WebContents::CreateParams(profile_
)));
598 contents_delegate_
.reset(new StartPageWebContentsDelegate(profile_
));
599 contents_
->SetDelegate(contents_delegate_
.get());
601 // The ZoomController needs to be created before the web contents is observed
602 // by this object. Otherwise it will react to DidNavigateMainFrame after this
603 // object does, resetting the zoom mode in the process.
604 ui_zoom::ZoomController::CreateForWebContents(contents_
.get());
605 Observe(contents_
.get());
610 void StartPageService::UnloadContents() {
612 webui_finished_loading_
= false;
615 void StartPageService::LoadStartPageURL() {
616 contents_
->GetController().LoadURL(
617 GURL(chrome::kChromeUIAppListStartPageURL
),
619 ui::PAGE_TRANSITION_AUTO_TOPLEVEL
,
622 contents_
->GetRenderViewHost()->GetView()->SetBackgroundColor(
623 SK_ColorTRANSPARENT
);
626 void StartPageService::FetchDoodleJson() {
627 if (!search_engine_is_google_
)
630 GURL::Replacements replacements
;
631 replacements
.SetPathStr(kDoodleJsonPath
);
633 GURL
google_base_url(UIThreadSearchTermsData(profile_
).GoogleBaseURLValue());
634 GURL doodle_url
= google_base_url
.ReplaceComponents(replacements
);
636 net::URLFetcher::Create(0, doodle_url
, net::URLFetcher::GET
, this);
637 doodle_fetcher_
->SetRequestContext(profile_
->GetRequestContext());
638 doodle_fetcher_
->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES
);
639 doodle_fetcher_
->Start();
642 void StartPageService::OnURLFetchComplete(const net::URLFetcher
* source
) {
643 std::string json_data
;
644 source
->GetResponseAsString(&json_data
);
646 // Remove XSSI guard for JSON parsing.
647 size_t json_start_index
= json_data
.find("{");
648 base::StringPiece
json_data_substr(json_data
);
649 if (json_start_index
!= std::string::npos
)
650 json_data_substr
.remove_prefix(json_start_index
);
652 JSONStringValueDeserializer
deserializer(json_data_substr
);
653 deserializer
.set_allow_trailing_comma(true);
655 scoped_ptr
<base::Value
> doodle_json(
656 deserializer
.Deserialize(&error_code
, nullptr));
658 base::TimeDelta recheck_delay
;
659 if (error_code
!= 0) {
660 // On failure, use expotential backoff.
661 backoff_entry_
.InformOfRequest(false);
662 recheck_delay
= backoff_entry_
.GetTimeUntilRelease();
664 // If we received information, even if there's no doodle, reset the backoff
665 // entry and start rechecking for the doodle at the maximum interval.
666 backoff_entry_
.Reset();
667 recheck_delay
= base::TimeDelta::FromMilliseconds(kMaximumRecheckDelayMs
);
669 if (contents_
&& contents_
->GetWebUI()) {
670 contents_
->GetWebUI()->CallJavascriptFunction(
671 "appList.startPage.onAppListDoodleUpdated", *doodle_json
,
673 UIThreadSearchTermsData(profile_
).GoogleBaseURLValue()));
677 // Check for a new doodle.
678 content::BrowserThread::PostDelayedTask(
679 content::BrowserThread::UI
, FROM_HERE
,
680 base::Bind(&StartPageService::FetchDoodleJson
,
681 weak_factory_
.GetWeakPtr()),
685 } // namespace app_list