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