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 // 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
;
84 class StartPageService::ProfileDestroyObserver
85 : public content::NotificationObserver
{
87 explicit ProfileDestroyObserver(StartPageService
* 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.
95 this, chrome::NOTIFICATION_PROFILE_DESTROYED
,
96 content::Source
<Profile
>(service_
->profile()->GetOriginalProfile()));
99 chrome::NOTIFICATION_PROFILE_DESTROYED
,
100 content::Source
<Profile
>(service_
->profile()));
102 ~ProfileDestroyObserver() override
{}
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
{
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
,
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
154 disposition
== NEW_BACKGROUND_TAB
? disposition
: NEW_FOREGROUND_TAB
;
155 chrome::AddWebContents(displayer
.browser(),
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
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
;
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
;
187 DISALLOW_COPY_AND_ASSIGN(StartPageWebContentsDelegate
);
190 #if defined(OS_CHROMEOS)
192 class StartPageService::AudioStatus
193 : public chromeos::CrasAudioHandler::AudioObserver
{
195 explicit AudioStatus(StartPageService
* start_page_service
)
196 : start_page_service_(start_page_service
) {
197 chromeos::CrasAudioHandler::Get()->AddAudioObserver(this);
201 ~AudioStatus() override
{
202 chromeos::CrasAudioHandler::Get()->RemoveAudioObserver(this);
206 chromeos::CrasAudioHandler
* audio_handler
=
207 chromeos::CrasAudioHandler::Get();
208 return (audio_handler
->GetPrimaryActiveInputNode() != 0) &&
209 !audio_handler
->IsInputMuted();
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
{
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);
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);
270 StartPageService
* start_page_service_
;
271 net::NetworkChangeNotifier::ConnectionType last_type_
;
273 DISALLOW_COPY_AND_ASSIGN(NetworkChangeObserver
);
277 StartPageService
* StartPageService::Get(Profile
* profile
) {
278 return StartPageServiceFactory::GetForProfile(profile
);
281 StartPageService::StartPageService(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
);
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() {
358 bool StartPageService::ShouldEnableSpeechRecognition() const {
359 return microphone_available_
&& network_available_
;
362 void StartPageService::AppListShown() {
365 } else if (contents_
->IsCrashed()) {
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));
377 void StartPageService::AppListHidden() {
378 if (!app_list::switches::IsExperimentalAppListEnabled())
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
389 OnSpeechRecognitionStateChanged(SPEECH_RECOGNITION_READY
);
392 #if defined(OS_CHROMEOS)
393 audio_status_
.reset();
397 void StartPageService::ToggleSpeechRecognition(
398 const scoped_refptr
<content::SpeechRecognitionSessionPreamble
>& preamble
) {
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
);
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(),
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
&&
427 (service
->IsSometimesOnEnabled() || service
->IsAlwaysOnEnabled()) &&
428 service
->IsServiceAvailable();
434 content::WebContents
* StartPageService::GetStartPageContents() {
435 return app_list::switches::IsExperimentalAppListEnabled() ? contents_
.get()
439 content::WebContents
* StartPageService::GetSpeechRecognitionContents() {
440 if (app_list::switches::IsVoiceSearchEnabled()) {
443 return contents_
.get();
448 void StartPageService::OnSpeechResult(
449 const base::string16
& query
, bool is_final
) {
451 speech_result_obtained_
= true;
452 RecordAction(UserMetricsAction("AppList_SearchedBySpeech"));
454 FOR_EACH_OBSERVER(StartPageObserver
,
456 OnSpeechResult(query
, is_final
));
459 void StartPageService::OnSpeechSoundLevelChanged(int16_t level
) {
460 FOR_EACH_OBSERVER(StartPageObserver
,
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
;
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
)
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"));
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;
501 FOR_EACH_OBSERVER(StartPageObserver
,
503 OnSpeechRecognitionStateChanged(new_state
));
506 void StartPageService::GetSpeechAuthParameters(std::string
* auth_scope
,
507 std::string
* auth_token
) {
508 HotwordService
* service
= HotwordServiceFactory::GetForProfile(profile_
);
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() {
520 #if defined(OS_CHROMEOS)
521 audio_status_
.reset();
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
550 webui_finished_loading_
= true;
551 for (const auto& cb
: pending_webui_callbacks_
)
553 pending_webui_callbacks_
.clear();
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());
573 void StartPageService::UnloadContents() {
575 webui_finished_loading_
= false;
578 void StartPageService::LoadStartPageURL() {
579 contents_
->GetController().LoadURL(
580 GURL(chrome::kChromeUIAppListStartPageURL
),
582 ui::PAGE_TRANSITION_AUTO_TOPLEVEL
,
585 contents_
->GetRenderViewHost()->GetView()->SetBackgroundColor(
586 SK_ColorTRANSPARENT
);
589 void StartPageService::FetchDoodleJson() {
590 if (!search_engine_is_google_
)
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);
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",
631 recheck_delay
= base::TimeDelta::FromMilliseconds(time_to_live
);
635 if (contents_
&& contents_
->GetWebUI()) {
636 contents_
->GetWebUI()->CallJavascriptFunction(
637 "appList.startPage.onAppListDoodleUpdated", *doodle_json
,
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()),
651 } // namespace app_list