OLD | NEW |
| (Empty) |
1 // Copyright 2016 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/chromeos/arc/arc_navigation_throttle.h" | |
6 | |
7 #include <algorithm> | |
8 | |
9 #include "base/bind.h" | |
10 #include "base/logging.h" | |
11 #include "base/memory/ref_counted.h" | |
12 #include "base/metrics/histogram_macros.h" | |
13 #include "components/arc/arc_bridge_service.h" | |
14 #include "components/arc/arc_service_manager.h" | |
15 #include "components/arc/intent_helper/arc_intent_helper_bridge.h" | |
16 #include "components/arc/intent_helper/local_activity_resolver.h" | |
17 #include "components/arc/intent_helper/page_transition_util.h" | |
18 #include "content/public/browser/browser_thread.h" | |
19 #include "content/public/browser/navigation_controller.h" | |
20 #include "content/public/browser/navigation_handle.h" | |
21 #include "content/public/browser/web_contents.h" | |
22 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" | |
23 #include "ui/base/page_transition_types.h" | |
24 #include "url/gurl.h" | |
25 | |
26 namespace arc { | |
27 | |
28 namespace { | |
29 | |
30 constexpr uint32_t kMinVersionForHandleUrl = 2; | |
31 constexpr uint32_t kMinVersionForRequestUrlHandlerList = 2; | |
32 constexpr uint32_t kMinVersionForAddPreferredPackage = 7; | |
33 | |
34 scoped_refptr<ActivityIconLoader> GetIconLoader() { | |
35 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
36 ArcServiceManager* arc_service_manager = ArcServiceManager::Get(); | |
37 return arc_service_manager ? arc_service_manager->icon_loader() : nullptr; | |
38 } | |
39 | |
40 // Compares the host name of the referrer and target URL to decide whether | |
41 // the navigation needs to be overriden. | |
42 bool ShouldOverrideUrlLoading(const GURL& previous_url, | |
43 const GURL& current_url) { | |
44 // When the navigation is initiated in a web page where sending a referrer | |
45 // is disabled, |previous_url| can be empty. In this case, we should open | |
46 // it in the desktop browser. | |
47 if (!previous_url.is_valid() || previous_url.is_empty()) | |
48 return false; | |
49 | |
50 // Also check |current_url| just in case. | |
51 if (!current_url.is_valid() || current_url.is_empty()) { | |
52 DVLOG(1) << "Unexpected URL: " << current_url << ", opening it in Chrome."; | |
53 return false; | |
54 } | |
55 | |
56 return !net::registry_controlled_domains::SameDomainOrHost( | |
57 current_url, previous_url, | |
58 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); | |
59 } | |
60 | |
61 // Returns true if |handlers| contain one or more apps. When this function is | |
62 // called from OnAppCandidatesReceived, |handlers| always contain Chrome (aka | |
63 // intent_helper), but the function doesn't treat it as an app. | |
64 bool IsAppAvailable(const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers) { | |
65 return handlers.size() > 1 || (handlers.size() == 1 && | |
66 !ArcIntentHelperBridge::IsIntentHelperPackage( | |
67 handlers[0]->package_name)); | |
68 } | |
69 | |
70 // Searches for a preferred app in |handlers| and returns its index. If not | |
71 // found, returns |handlers.size()|. | |
72 size_t FindPreferredApp( | |
73 const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers, | |
74 const GURL& url_for_logging) { | |
75 for (size_t i = 0; i < handlers.size(); ++i) { | |
76 if (!handlers[i]->is_preferred) | |
77 continue; | |
78 if (ArcIntentHelperBridge::IsIntentHelperPackage( | |
79 handlers[i]->package_name)) { | |
80 // If Chrome browser was selected as the preferred app, we shouldn't | |
81 // create a throttle. | |
82 DVLOG(1) | |
83 << "Chrome browser is selected as the preferred app for this URL: " | |
84 << url_for_logging; | |
85 } | |
86 return i; | |
87 } | |
88 return handlers.size(); // not found | |
89 } | |
90 | |
91 // Swaps Chrome app with any app in row |kMaxAppResults-1| iff its index is | |
92 // bigger, thus ensuring the user can always see Chrome without scrolling. | |
93 // When swap is needed, fills |out_indices| and returns true. If |handlers| | |
94 // do not have Chrome, returns false. | |
95 bool IsSwapElementsNeeded( | |
96 const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers, | |
97 std::pair<size_t, size_t>* out_indices) { | |
98 size_t chrome_app_index = 0; | |
99 for (size_t i = 0; i < handlers.size(); ++i) { | |
100 if (ArcIntentHelperBridge::IsIntentHelperPackage( | |
101 handlers[i]->package_name)) { | |
102 chrome_app_index = i; | |
103 break; | |
104 } | |
105 } | |
106 if (chrome_app_index < ArcNavigationThrottle::kMaxAppResults) | |
107 return false; | |
108 | |
109 *out_indices = std::make_pair(ArcNavigationThrottle::kMaxAppResults - 1, | |
110 chrome_app_index); | |
111 return true; | |
112 } | |
113 | |
114 } // namespace | |
115 | |
116 ArcNavigationThrottle::ArcNavigationThrottle( | |
117 content::NavigationHandle* navigation_handle, | |
118 const ShowIntentPickerCallback& show_intent_picker_cb) | |
119 : content::NavigationThrottle(navigation_handle), | |
120 show_intent_picker_callback_(show_intent_picker_cb), | |
121 previous_user_action_(CloseReason::INVALID), | |
122 weak_ptr_factory_(this) {} | |
123 | |
124 ArcNavigationThrottle::~ArcNavigationThrottle() = default; | |
125 | |
126 content::NavigationThrottle::ThrottleCheckResult | |
127 ArcNavigationThrottle::WillStartRequest() { | |
128 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
129 // We must not handle navigations started from the context menu. | |
130 if (navigation_handle()->WasStartedFromContextMenu()) | |
131 return content::NavigationThrottle::PROCEED; | |
132 return HandleRequest(); | |
133 } | |
134 | |
135 content::NavigationThrottle::ThrottleCheckResult | |
136 ArcNavigationThrottle::WillRedirectRequest() { | |
137 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
138 | |
139 switch (previous_user_action_) { | |
140 case CloseReason::ERROR: | |
141 case CloseReason::DIALOG_DEACTIVATED: | |
142 // User dismissed the dialog, or some error occurred before. Don't | |
143 // repeatedly pop up the dialog. | |
144 return content::NavigationThrottle::PROCEED; | |
145 | |
146 case CloseReason::ALWAYS_PRESSED: | |
147 case CloseReason::JUST_ONCE_PRESSED: | |
148 case CloseReason::PREFERRED_ACTIVITY_FOUND: | |
149 // Should never get here - if the user selected one of these previously, | |
150 // Chrome should not see a redirect. | |
151 NOTREACHED(); | |
152 | |
153 case CloseReason::INVALID: | |
154 // No picker has previously been popped up for this - continue. | |
155 break; | |
156 } | |
157 return HandleRequest(); | |
158 } | |
159 | |
160 content::NavigationThrottle::ThrottleCheckResult | |
161 ArcNavigationThrottle::HandleRequest() { | |
162 const GURL& url = navigation_handle()->GetURL(); | |
163 | |
164 // Always handle http(s) <form> submissions in Chrome for two reasons: 1) we | |
165 // don't have a way to send POST data to ARC, and 2) intercepting http(s) form | |
166 // submissions is not very important because such submissions are usually | |
167 // done within the same domain. ShouldOverrideUrlLoading() below filters out | |
168 // such submissions anyway. | |
169 constexpr bool kAllowFormSubmit = false; | |
170 | |
171 if (ShouldIgnoreNavigation(navigation_handle()->GetPageTransition(), | |
172 kAllowFormSubmit)) | |
173 return content::NavigationThrottle::PROCEED; | |
174 | |
175 const GURL referrer_url = navigation_handle()->GetReferrer().url; | |
176 const GURL current_url = navigation_handle()->GetURL(); | |
177 const GURL last_committed_url = | |
178 navigation_handle()->GetWebContents()->GetLastCommittedURL(); | |
179 | |
180 // For navigations from http to https we clean up the Referrer as part of the | |
181 // sanitization proccess, however we may still have access to the last | |
182 // committed URL. On the other hand, navigations started within a new tab | |
183 // (e.g. due to target="_blank") will keep no track of any previous entries | |
184 // and so GetLastCommittedURL() can be seen empty sometimes, this is why we | |
185 // use one or the other accordingly. Also we don't use GetVisibleURL() since | |
186 // it may contain a still non-committed URL (i.e. it can be the same as | |
187 // GetURL()). | |
188 const GURL previous_url = | |
189 referrer_url.is_empty() ? last_committed_url : referrer_url; | |
190 | |
191 if (!ShouldOverrideUrlLoading(previous_url, current_url)) | |
192 return content::NavigationThrottle::PROCEED; | |
193 | |
194 ArcServiceManager* arc_service_manager = ArcServiceManager::Get(); | |
195 DCHECK(arc_service_manager); | |
196 scoped_refptr<LocalActivityResolver> local_resolver = | |
197 arc_service_manager->activity_resolver(); | |
198 if (local_resolver->ShouldChromeHandleUrl(url)) { | |
199 // Allow navigation to proceed if there isn't an android app that handles | |
200 // the given URL. | |
201 return content::NavigationThrottle::PROCEED; | |
202 } | |
203 | |
204 auto* instance = ArcIntentHelperBridge::GetIntentHelperInstance( | |
205 "RequestUrlHandlerList", kMinVersionForRequestUrlHandlerList); | |
206 if (!instance) | |
207 return content::NavigationThrottle::PROCEED; | |
208 instance->RequestUrlHandlerList( | |
209 url.spec(), base::Bind(&ArcNavigationThrottle::OnAppCandidatesReceived, | |
210 weak_ptr_factory_.GetWeakPtr())); | |
211 return content::NavigationThrottle::DEFER; | |
212 } | |
213 | |
214 // We received the array of app candidates to handle this URL (even the Chrome | |
215 // app is included). | |
216 void ArcNavigationThrottle::OnAppCandidatesReceived( | |
217 mojo::Array<mojom::IntentHandlerInfoPtr> handlers) { | |
218 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
219 if (!IsAppAvailable(handlers)) { | |
220 // This scenario shouldn't be accesed as ArcNavigationThrottle is created | |
221 // iff there are ARC apps which can actually handle the given URL. | |
222 DVLOG(1) << "There are no app candidates for this URL: " | |
223 << navigation_handle()->GetURL(); | |
224 navigation_handle()->Resume(); | |
225 return; | |
226 } | |
227 | |
228 // If one of the apps is marked as preferred, use it right away without | |
229 // showing the UI. | |
230 const size_t index = | |
231 FindPreferredApp(handlers, navigation_handle()->GetURL()); | |
232 if (index != handlers.size()) { | |
233 const std::string package_name = handlers[index]->package_name; | |
234 OnIntentPickerClosed(std::move(handlers), package_name, | |
235 CloseReason::PREFERRED_ACTIVITY_FOUND); | |
236 return; | |
237 } | |
238 | |
239 std::pair<size_t, size_t> indices; | |
240 if (IsSwapElementsNeeded(handlers, &indices)) | |
241 std::swap(handlers[indices.first], handlers[indices.second]); | |
242 | |
243 scoped_refptr<ActivityIconLoader> icon_loader = GetIconLoader(); | |
244 if (!icon_loader) { | |
245 LOG(ERROR) << "Cannot get an instance of ActivityIconLoader"; | |
246 navigation_handle()->Resume(); | |
247 return; | |
248 } | |
249 std::vector<ActivityIconLoader::ActivityName> activities; | |
250 for (const auto& handler : handlers) | |
251 activities.emplace_back(handler->package_name, handler->activity_name); | |
252 icon_loader->GetActivityIcons( | |
253 activities, | |
254 base::Bind(&ArcNavigationThrottle::OnAppIconsReceived, | |
255 weak_ptr_factory_.GetWeakPtr(), base::Passed(&handlers))); | |
256 } | |
257 | |
258 void ArcNavigationThrottle::OnAppIconsReceived( | |
259 mojo::Array<mojom::IntentHandlerInfoPtr> handlers, | |
260 std::unique_ptr<ActivityIconLoader::ActivityToIconsMap> icons) { | |
261 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
262 std::vector<AppInfo> app_info; | |
263 | |
264 for (const auto& handler : handlers) { | |
265 gfx::Image icon; | |
266 const ActivityIconLoader::ActivityName activity(handler->package_name, | |
267 handler->activity_name); | |
268 const auto it = icons->find(activity); | |
269 | |
270 app_info.emplace_back( | |
271 AppInfo(it != icons->end() ? it->second.icon20 : gfx::Image(), | |
272 handler->package_name, handler->name)); | |
273 } | |
274 | |
275 show_intent_picker_callback_.Run( | |
276 navigation_handle()->GetWebContents(), app_info, | |
277 base::Bind(&ArcNavigationThrottle::OnIntentPickerClosed, | |
278 weak_ptr_factory_.GetWeakPtr(), base::Passed(&handlers))); | |
279 } | |
280 | |
281 void ArcNavigationThrottle::OnIntentPickerClosed( | |
282 mojo::Array<mojom::IntentHandlerInfoPtr> handlers, | |
283 const std::string& selected_app_package, | |
284 CloseReason close_reason) { | |
285 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
286 const GURL& url = navigation_handle()->GetURL(); | |
287 content::NavigationHandle* handle = navigation_handle(); | |
288 previous_user_action_ = close_reason; | |
289 | |
290 // Make sure that the instance at least supports HandleUrl. | |
291 auto* instance = ArcIntentHelperBridge::GetIntentHelperInstance( | |
292 "HandleUrl", kMinVersionForHandleUrl); | |
293 // Since we are selecting an app by its package name, we need to locate it | |
294 // on the |handlers| structure before sending the IPC to ARC. | |
295 const size_t selected_app_index = GetAppIndex(handlers, selected_app_package); | |
296 if (!instance) { | |
297 close_reason = CloseReason::ERROR; | |
298 } else if (close_reason == CloseReason::JUST_ONCE_PRESSED || | |
299 close_reason == CloseReason::ALWAYS_PRESSED || | |
300 close_reason == CloseReason::PREFERRED_ACTIVITY_FOUND) { | |
301 if (selected_app_index == handlers.size()) | |
302 close_reason = CloseReason::ERROR; | |
303 } | |
304 | |
305 switch (close_reason) { | |
306 case CloseReason::ERROR: | |
307 case CloseReason::DIALOG_DEACTIVATED: { | |
308 // If the user fails to select an option from the list, or the UI returned | |
309 // an error or if |selected_app_index| is not a valid index, then resume | |
310 // the navigation in Chrome. | |
311 DVLOG(1) << "User didn't select a valid option, resuming navigation."; | |
312 handle->Resume(); | |
313 break; | |
314 } | |
315 case CloseReason::ALWAYS_PRESSED: { | |
316 // Call AddPreferredPackage if it is supported. Reusing the same | |
317 // |instance| is okay. | |
318 if (ArcIntentHelperBridge::GetIntentHelperInstance( | |
319 "AddPreferredPackage", kMinVersionForAddPreferredPackage)) { | |
320 instance->AddPreferredPackage( | |
321 handlers[selected_app_index]->package_name); | |
322 } | |
323 // fall through. | |
324 } | |
325 case CloseReason::JUST_ONCE_PRESSED: | |
326 case CloseReason::PREFERRED_ACTIVITY_FOUND: { | |
327 if (ArcIntentHelperBridge::IsIntentHelperPackage( | |
328 handlers[selected_app_index]->package_name)) { | |
329 handle->Resume(); | |
330 } else { | |
331 instance->HandleUrl(url.spec(), selected_app_package); | |
332 handle->CancelDeferredNavigation( | |
333 content::NavigationThrottle::CANCEL_AND_IGNORE); | |
334 if (handle->GetWebContents()->GetController().IsInitialNavigation()) | |
335 handle->GetWebContents()->Close(); | |
336 } | |
337 break; | |
338 } | |
339 case CloseReason::INVALID: { | |
340 NOTREACHED(); | |
341 return; | |
342 } | |
343 } | |
344 | |
345 UMA_HISTOGRAM_ENUMERATION("Arc.IntentHandlerAction", | |
346 static_cast<int>(close_reason), | |
347 static_cast<int>(CloseReason::SIZE)); | |
348 } | |
349 | |
350 // static | |
351 size_t ArcNavigationThrottle::GetAppIndex( | |
352 const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers, | |
353 const std::string& selected_app_package) { | |
354 for (size_t i = 0; i < handlers.size(); ++i) { | |
355 if (handlers[i]->package_name == selected_app_package) | |
356 return i; | |
357 } | |
358 return handlers.size(); | |
359 } | |
360 | |
361 // static | |
362 bool ArcNavigationThrottle::ShouldOverrideUrlLoadingForTesting( | |
363 const GURL& previous_url, | |
364 const GURL& current_url) { | |
365 return ShouldOverrideUrlLoading(previous_url, current_url); | |
366 } | |
367 | |
368 // static | |
369 bool ArcNavigationThrottle::IsAppAvailableForTesting( | |
370 const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers) { | |
371 return IsAppAvailable(handlers); | |
372 } | |
373 | |
374 // static | |
375 size_t ArcNavigationThrottle::FindPreferredAppForTesting( | |
376 const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers) { | |
377 return FindPreferredApp(handlers, GURL()); | |
378 } | |
379 | |
380 // static | |
381 bool ArcNavigationThrottle::IsSwapElementsNeededForTesting( | |
382 const mojo::Array<mojom::IntentHandlerInfoPtr>& handlers, | |
383 std::pair<size_t, size_t>* out_indices) { | |
384 return IsSwapElementsNeeded(handlers, out_indices); | |
385 } | |
386 | |
387 } // namespace arc | |
OLD | NEW |