OLD | NEW |
| (Empty) |
1 // Copyright 2017 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/app_list/search_answer_web_contents_delegate.h" | |
6 | |
7 #include "base/command_line.h" | |
8 #include "base/metrics/histogram_macros.h" | |
9 #include "base/metrics/user_metrics.h" | |
10 #include "base/strings/utf_string_conversions.h" | |
11 #include "chrome/browser/profiles/profile.h" | |
12 #include "chrome/browser/ui/browser_navigator.h" | |
13 #include "chrome/browser/ui/browser_navigator_params.h" | |
14 #include "content/public/browser/navigation_handle.h" | |
15 #include "content/public/browser/render_view_host.h" | |
16 #include "content/public/browser/web_contents.h" | |
17 #include "content/public/browser/web_contents_delegate.h" | |
18 #include "content/public/common/renderer_preferences.h" | |
19 #include "net/http/http_response_headers.h" | |
20 #include "net/http/http_status_code.h" | |
21 #include "ui/app_list/app_list_features.h" | |
22 #include "ui/app_list/app_list_model.h" | |
23 #include "ui/app_list/search_box_model.h" | |
24 #include "ui/app_list/search_result.h" | |
25 #include "ui/views/controls/webview/web_contents_set_background_color.h" | |
26 #include "ui/views/controls/webview/webview.h" | |
27 #include "ui/views/widget/widget.h" | |
28 | |
29 namespace app_list { | |
30 | |
31 namespace { | |
32 | |
33 enum class SearchAnswerRequestResult { | |
34 REQUEST_RESULT_ANOTHER_REQUEST_STARTED = 0, | |
35 REQUEST_RESULT_REQUEST_FAILED = 1, | |
36 REQUEST_RESULT_NO_ANSWER = 2, | |
37 REQUEST_RESULT_RECEIVED_ANSWER = 3, | |
38 REQUEST_RESULT_RECEIVED_ANSWER_TOO_LARGE = 4, | |
39 REQUEST_RESULT_MAX = 5 | |
40 }; | |
41 | |
42 void RecordRequestResult(SearchAnswerRequestResult request_result) { | |
43 UMA_HISTOGRAM_ENUMERATION("SearchAnswer.RequestResult", request_result, | |
44 SearchAnswerRequestResult::REQUEST_RESULT_MAX); | |
45 } | |
46 | |
47 class SearchAnswerWebView : public views::WebView { | |
48 public: | |
49 explicit SearchAnswerWebView(content::BrowserContext* browser_context) | |
50 : WebView(browser_context) {} | |
51 | |
52 // views::WebView overrides: | |
53 void VisibilityChanged(View* starting_from, bool is_visible) override { | |
54 WebView::VisibilityChanged(starting_from, is_visible); | |
55 | |
56 if (GetWidget() && GetWidget()->IsVisible() && IsDrawn()) { | |
57 if (shown_time_.is_null()) | |
58 shown_time_ = base::TimeTicks::Now(); | |
59 } else { | |
60 if (!shown_time_.is_null()) { | |
61 UMA_HISTOGRAM_MEDIUM_TIMES("SearchAnswer.AnswerVisibleTime", | |
62 base::TimeTicks::Now() - shown_time_); | |
63 shown_time_ = base::TimeTicks(); | |
64 } | |
65 } | |
66 } | |
67 | |
68 const char* GetClassName() const override { return "SearchAnswerWebView"; } | |
69 | |
70 private: | |
71 // Time when the answer became visible to the user. | |
72 base::TimeTicks shown_time_; | |
73 | |
74 DISALLOW_COPY_AND_ASSIGN(SearchAnswerWebView); | |
75 }; | |
76 | |
77 class SearchAnswerResult : public SearchResult { | |
78 public: | |
79 SearchAnswerResult(Profile* profile, | |
80 const std::string& result_url, | |
81 views::View* web_view) | |
82 : profile_(profile) { | |
83 set_display_type(DISPLAY_CARD); | |
84 set_id(result_url); | |
85 set_relevance(1); | |
86 set_view(web_view); | |
87 } | |
88 | |
89 // SearchResult overrides: | |
90 std::unique_ptr<SearchResult> Duplicate() const override { | |
91 return base::MakeUnique<SearchAnswerResult>(profile_, id(), view()); | |
92 } | |
93 | |
94 void Open(int event_flags) override { | |
95 chrome::NavigateParams params(profile_, GURL(id()), | |
96 ui::PAGE_TRANSITION_GENERATED); | |
97 params.disposition = ui::DispositionFromEventFlags(event_flags); | |
98 chrome::Navigate(¶ms); | |
99 } | |
100 | |
101 private: | |
102 Profile* const profile_; | |
103 }; | |
104 | |
105 } // namespace | |
106 | |
107 SearchAnswerWebContentsDelegate::SearchAnswerWebContentsDelegate( | |
108 Profile* profile, | |
109 app_list::AppListModel* model) | |
110 : profile_(profile), | |
111 model_(model), | |
112 web_view_(base::MakeUnique<SearchAnswerWebView>(profile)), | |
113 web_contents_( | |
114 content::WebContents::Create(content::WebContents::CreateParams( | |
115 profile, | |
116 content::SiteInstance::Create(profile)))), | |
117 answer_server_url_(features::AnswerServerUrl()) { | |
118 content::RendererPreferences* renderer_prefs = | |
119 web_contents_->GetMutableRendererPrefs(); | |
120 renderer_prefs->can_accept_load_drops = false; | |
121 // We need the OpenURLFromTab() to get called. | |
122 renderer_prefs->browser_handles_all_top_level_requests = true; | |
123 web_contents_->GetRenderViewHost()->SyncRendererPrefs(); | |
124 | |
125 Observe(web_contents_.get()); | |
126 web_contents_->SetDelegate(this); | |
127 web_view_->set_owned_by_client(); | |
128 web_view_->SetWebContents(web_contents_.get()); | |
129 | |
130 // Make the webview transparent since it's going to be shown on top of a | |
131 // highlightable button. | |
132 views::WebContentsSetBackgroundColor::CreateForWebContentsWithColor( | |
133 web_contents_.get(), SK_ColorTRANSPARENT); | |
134 } | |
135 | |
136 SearchAnswerWebContentsDelegate::~SearchAnswerWebContentsDelegate() { | |
137 RecordReceivedAnswerFinalResult(); | |
138 } | |
139 | |
140 views::View* SearchAnswerWebContentsDelegate::web_view() { | |
141 return web_view_.get(); | |
142 } | |
143 | |
144 void SearchAnswerWebContentsDelegate::Start(bool is_voice_query, | |
145 const base::string16& query) { | |
146 RecordReceivedAnswerFinalResult(); | |
147 // Reset the state. | |
148 received_answer_ = false; | |
149 OnResultAvailable(false); | |
150 current_request_url_ = GURL(); | |
151 server_request_start_time_ = answer_loaded_time_ = base::TimeTicks(); | |
152 | |
153 if (is_voice_query) { | |
154 // No need to send a server request and show a card because launcher | |
155 // automatically closes upon voice queries. | |
156 return; | |
157 } | |
158 | |
159 if (!model_->search_engine_is_google()) | |
160 return; | |
161 | |
162 if (query.empty()) | |
163 return; | |
164 | |
165 // Start a request to the answer server. | |
166 | |
167 // Lifetime of |prefixed_query| should be longer than the one of | |
168 // |replacements|. | |
169 base::string16 prefixed_query(base::UTF8ToUTF16("q=") + query); | |
170 GURL::ReplacementsW replacements; | |
171 replacements.SetQueryStr(prefixed_query); | |
172 current_request_url_ = answer_server_url_.ReplaceComponents(replacements); | |
173 | |
174 content::NavigationController::LoadURLParams load_params( | |
175 current_request_url_); | |
176 load_params.transition_type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL; | |
177 load_params.should_clear_history_list = true; | |
178 web_contents_->GetController().LoadURLWithParams(load_params); | |
179 server_request_start_time_ = base::TimeTicks::Now(); | |
180 | |
181 // We are going to call WebContents::GetPreferredSize(). | |
182 web_contents_->GetRenderViewHost()->EnablePreferredSizeMode(); | |
183 } | |
184 | |
185 void SearchAnswerWebContentsDelegate::UpdatePreferredSize( | |
186 content::WebContents* web_contents, | |
187 const gfx::Size& pref_size) { | |
188 OnResultAvailable(received_answer_ && IsCardSizeOk() && | |
189 !web_contents_->IsLoading()); | |
190 web_view_->SetPreferredSize(pref_size); | |
191 if (!answer_loaded_time_.is_null()) { | |
192 UMA_HISTOGRAM_TIMES("SearchAnswer.ResizeAfterLoadTime", | |
193 base::TimeTicks::Now() - answer_loaded_time_); | |
194 } | |
195 } | |
196 | |
197 content::WebContents* SearchAnswerWebContentsDelegate::OpenURLFromTab( | |
198 content::WebContents* source, | |
199 const content::OpenURLParams& params) { | |
200 if (!params.user_gesture) | |
201 return WebContentsDelegate::OpenURLFromTab(source, params); | |
202 | |
203 // Open the user-clicked link in the browser taking into account the requested | |
204 // disposition. | |
205 chrome::NavigateParams new_tab_params(profile_, params.url, | |
206 params.transition); | |
207 | |
208 new_tab_params.disposition = params.disposition; | |
209 | |
210 if (params.disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB) { | |
211 // When the user asks to open a link as a background tab, we show an | |
212 // activated window with the new activated tab after the user closes the | |
213 // launcher. So it's "background" relative to the launcher itself. | |
214 new_tab_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; | |
215 new_tab_params.window_action = chrome::NavigateParams::SHOW_WINDOW_INACTIVE; | |
216 } | |
217 | |
218 chrome::Navigate(&new_tab_params); | |
219 | |
220 base::RecordAction(base::UserMetricsAction("SearchAnswer_OpenedUrl")); | |
221 | |
222 return new_tab_params.target_contents; | |
223 } | |
224 | |
225 bool SearchAnswerWebContentsDelegate::HandleContextMenu( | |
226 const content::ContextMenuParams& params) { | |
227 // Disable showing the menu. | |
228 return true; | |
229 } | |
230 | |
231 void SearchAnswerWebContentsDelegate::DidFinishNavigation( | |
232 content::NavigationHandle* navigation_handle) { | |
233 if (navigation_handle->GetURL() != current_request_url_) { | |
234 RecordRequestResult( | |
235 SearchAnswerRequestResult::REQUEST_RESULT_ANOTHER_REQUEST_STARTED); | |
236 return; | |
237 } | |
238 | |
239 if (!navigation_handle->HasCommitted() || navigation_handle->IsErrorPage() || | |
240 !navigation_handle->IsInMainFrame()) { | |
241 RecordRequestResult( | |
242 SearchAnswerRequestResult::REQUEST_RESULT_REQUEST_FAILED); | |
243 return; | |
244 } | |
245 | |
246 if (!features::IsAnswerCardDarkRunEnabled()) { | |
247 if (!ParseResponseHeaders(navigation_handle->GetResponseHeaders())) { | |
248 RecordRequestResult(SearchAnswerRequestResult::REQUEST_RESULT_NO_ANSWER); | |
249 return; | |
250 } | |
251 } else { | |
252 // In the dark run mode, every other "server response" contains a card. | |
253 dark_run_received_answer_ = !dark_run_received_answer_; | |
254 if (!dark_run_received_answer_) | |
255 return; | |
256 // SearchResult requires a non-empty id. This "url" will never be opened. | |
257 result_url_ = "some string"; | |
258 } | |
259 | |
260 received_answer_ = true; | |
261 UMA_HISTOGRAM_TIMES("SearchAnswer.NavigationTime", | |
262 base::TimeTicks::Now() - server_request_start_time_); | |
263 } | |
264 | |
265 void SearchAnswerWebContentsDelegate::DidStopLoading() { | |
266 if (!received_answer_) | |
267 return; | |
268 | |
269 if (IsCardSizeOk()) | |
270 OnResultAvailable(true); | |
271 answer_loaded_time_ = base::TimeTicks::Now(); | |
272 UMA_HISTOGRAM_TIMES("SearchAnswer.LoadingTime", | |
273 answer_loaded_time_ - server_request_start_time_); | |
274 base::RecordAction(base::UserMetricsAction("SearchAnswer_StoppedLoading")); | |
275 } | |
276 | |
277 void SearchAnswerWebContentsDelegate::DidGetUserInteraction( | |
278 const blink::WebInputEvent::Type type) { | |
279 base::RecordAction(base::UserMetricsAction("SearchAnswer_UserInteraction")); | |
280 } | |
281 | |
282 bool SearchAnswerWebContentsDelegate::IsCardSizeOk() const { | |
283 if (features::IsAnswerCardDarkRunEnabled()) | |
284 return true; | |
285 | |
286 const gfx::Size size = web_contents_->GetPreferredSize(); | |
287 return size.width() <= features::AnswerCardMaxWidth() && | |
288 size.height() <= features::AnswerCardMaxHeight(); | |
289 } | |
290 | |
291 void SearchAnswerWebContentsDelegate::RecordReceivedAnswerFinalResult() { | |
292 // Recording whether a server response with an answer contains a card of a | |
293 // fitting size, or a too large one. Cannot do this in DidStopLoading() or | |
294 // UpdatePreferredSize() because this may be followed by a resizing with | |
295 // different dimensions, so this method gets called when card's life ends. | |
296 if (!received_answer_) | |
297 return; | |
298 | |
299 RecordRequestResult( | |
300 IsCardSizeOk() ? SearchAnswerRequestResult::REQUEST_RESULT_RECEIVED_ANSWER | |
301 : SearchAnswerRequestResult:: | |
302 REQUEST_RESULT_RECEIVED_ANSWER_TOO_LARGE); | |
303 } | |
304 | |
305 void SearchAnswerWebContentsDelegate::OnResultAvailable(bool is_available) { | |
306 SearchProvider::Results results; | |
307 if (is_available) { | |
308 results.reserve(1); | |
309 results.emplace_back(base::MakeUnique<SearchAnswerResult>( | |
310 profile_, result_url_, web_view_.get())); | |
311 } | |
312 SwapResults(&results); | |
313 } | |
314 | |
315 bool SearchAnswerWebContentsDelegate::ParseResponseHeaders( | |
316 const net::HttpResponseHeaders* headers) { | |
317 if (!headers || headers->response_code() != net::HTTP_OK) | |
318 return false; | |
319 if (!headers->HasHeaderValue("SearchAnswer-HasResult", "true")) | |
320 return false; | |
321 if (!headers->GetNormalizedHeader("SearchAnswer-OpenResultUrl", &result_url_)) | |
322 return false; | |
323 return true; | |
324 } | |
325 | |
326 } // namespace app_list | |
OLD | NEW |