| 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 |