| 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 } // namespace | |
| 62 | |
| 63 ArcNavigationThrottle::ArcNavigationThrottle( | |
| 64 content::NavigationHandle* navigation_handle, | |
| 65 const ShowIntentPickerCallback& show_intent_picker_cb) | |
| 66 : content::NavigationThrottle(navigation_handle), | |
| 67 show_intent_picker_callback_(show_intent_picker_cb), | |
| 68 previous_user_action_(CloseReason::INVALID), | |
| 69 weak_ptr_factory_(this) {} | |
| 70 | |
| 71 ArcNavigationThrottle::~ArcNavigationThrottle() = default; | |
| 72 | |
| 73 content::NavigationThrottle::ThrottleCheckResult | |
| 74 ArcNavigationThrottle::WillStartRequest() { | |
| 75 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 76 // We must not handle navigations started from the context menu. | |
| 77 if (navigation_handle()->WasStartedFromContextMenu()) | |
| 78 return content::NavigationThrottle::PROCEED; | |
| 79 return HandleRequest(); | |
| 80 } | |
| 81 | |
| 82 content::NavigationThrottle::ThrottleCheckResult | |
| 83 ArcNavigationThrottle::WillRedirectRequest() { | |
| 84 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 85 | |
| 86 switch (previous_user_action_) { | |
| 87 case CloseReason::ERROR: | |
| 88 case CloseReason::DIALOG_DEACTIVATED: | |
| 89 // User dismissed the dialog, or some error occurred before. Don't | |
| 90 // repeatedly pop up the dialog. | |
| 91 return content::NavigationThrottle::PROCEED; | |
| 92 | |
| 93 case CloseReason::ALWAYS_PRESSED: | |
| 94 case CloseReason::JUST_ONCE_PRESSED: | |
| 95 case CloseReason::PREFERRED_ACTIVITY_FOUND: | |
| 96 // Should never get here - if the user selected one of these previously, | |
| 97 // Chrome should not see a redirect. | |
| 98 NOTREACHED(); | |
| 99 | |
| 100 case CloseReason::INVALID: | |
| 101 // No picker has previously been popped up for this - continue. | |
| 102 break; | |
| 103 } | |
| 104 return HandleRequest(); | |
| 105 } | |
| 106 | |
| 107 content::NavigationThrottle::ThrottleCheckResult | |
| 108 ArcNavigationThrottle::HandleRequest() { | |
| 109 const GURL& url = navigation_handle()->GetURL(); | |
| 110 | |
| 111 // Always handle http(s) <form> submissions in Chrome for two reasons: 1) we | |
| 112 // don't have a way to send POST data to ARC, and 2) intercepting http(s) form | |
| 113 // submissions is not very important because such submissions are usually | |
| 114 // done within the same domain. ShouldOverrideUrlLoading() below filters out | |
| 115 // such submissions anyway. | |
| 116 constexpr bool kAllowFormSubmit = false; | |
| 117 | |
| 118 if (ShouldIgnoreNavigation(navigation_handle()->GetPageTransition(), | |
| 119 kAllowFormSubmit)) | |
| 120 return content::NavigationThrottle::PROCEED; | |
| 121 | |
| 122 const GURL previous_url = navigation_handle()->GetReferrer().url; | |
| 123 const GURL current_url = navigation_handle()->GetURL(); | |
| 124 if (!ShouldOverrideUrlLoading(previous_url, current_url)) | |
| 125 return content::NavigationThrottle::PROCEED; | |
| 126 | |
| 127 ArcServiceManager* arc_service_manager = ArcServiceManager::Get(); | |
| 128 DCHECK(arc_service_manager); | |
| 129 scoped_refptr<LocalActivityResolver> local_resolver = | |
| 130 arc_service_manager->activity_resolver(); | |
| 131 if (local_resolver->ShouldChromeHandleUrl(url)) { | |
| 132 // Allow navigation to proceed if there isn't an android app that handles | |
| 133 // the given URL. | |
| 134 return content::NavigationThrottle::PROCEED; | |
| 135 } | |
| 136 | |
| 137 auto* instance = ArcIntentHelperBridge::GetIntentHelperInstance( | |
| 138 "RequestUrlHandlerList", kMinVersionForRequestUrlHandlerList); | |
| 139 if (!instance) | |
| 140 return content::NavigationThrottle::PROCEED; | |
| 141 instance->RequestUrlHandlerList( | |
| 142 url.spec(), base::Bind(&ArcNavigationThrottle::OnAppCandidatesReceived, | |
| 143 weak_ptr_factory_.GetWeakPtr())); | |
| 144 return content::NavigationThrottle::DEFER; | |
| 145 } | |
| 146 | |
| 147 // We received the array of app candidates to handle this URL (even the Chrome | |
| 148 // app is included). | |
| 149 void ArcNavigationThrottle::OnAppCandidatesReceived( | |
| 150 mojo::Array<mojom::IntentHandlerInfoPtr> handlers) { | |
| 151 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 152 if (handlers.empty() || | |
| 153 (handlers.size() == 1 && ArcIntentHelperBridge::IsIntentHelperPackage( | |
| 154 handlers[0]->package_name))) { | |
| 155 // This scenario shouldn't be accesed as ArcNavigationThrottle is created | |
| 156 // iff there are ARC apps which can actually handle the given URL. | |
| 157 DVLOG(1) << "There are no app candidates for this URL: " | |
| 158 << navigation_handle()->GetURL().spec(); | |
| 159 navigation_handle()->Resume(); | |
| 160 return; | |
| 161 } | |
| 162 | |
| 163 // If one of the apps is marked as preferred, use it right away without | |
| 164 // showing the UI. | |
| 165 for (size_t i = 0; i < handlers.size(); ++i) { | |
| 166 if (!handlers[i]->is_preferred) | |
| 167 continue; | |
| 168 if (ArcIntentHelperBridge::IsIntentHelperPackage( | |
| 169 handlers[i]->package_name)) { | |
| 170 // If Chrome browser was selected as the preferred app, we should't | |
| 171 // create a throttle. | |
| 172 DVLOG(1) | |
| 173 << "Chrome browser is selected as the preferred app for this URL: " | |
| 174 << navigation_handle()->GetURL().spec(); | |
| 175 } | |
| 176 OnIntentPickerClosed(std::move(handlers), i, | |
| 177 CloseReason::PREFERRED_ACTIVITY_FOUND); | |
| 178 return; | |
| 179 } | |
| 180 | |
| 181 // Swap Chrome app with any app in row |kMaxAppResults-1| iff its index is | |
| 182 // bigger, thus ensuring the user can always see Chrome without scrolling. | |
| 183 size_t chrome_app_index = 0; | |
| 184 for (size_t i = 0; i < handlers.size(); ++i) { | |
| 185 if (ArcIntentHelperBridge::IsIntentHelperPackage( | |
| 186 handlers[i]->package_name)) { | |
| 187 chrome_app_index = i; | |
| 188 break; | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 if (chrome_app_index >= kMaxAppResults) | |
| 193 std::swap(handlers[kMaxAppResults - 1], handlers[chrome_app_index]); | |
| 194 | |
| 195 scoped_refptr<ActivityIconLoader> icon_loader = GetIconLoader(); | |
| 196 if (!icon_loader) { | |
| 197 LOG(ERROR) << "Cannot get an instance of ActivityIconLoader"; | |
| 198 navigation_handle()->Resume(); | |
| 199 return; | |
| 200 } | |
| 201 std::vector<ActivityIconLoader::ActivityName> activities; | |
| 202 for (const auto& handler : handlers) { | |
| 203 activities.emplace_back(handler->package_name, handler->activity_name); | |
| 204 } | |
| 205 icon_loader->GetActivityIcons( | |
| 206 activities, | |
| 207 base::Bind(&ArcNavigationThrottle::OnAppIconsReceived, | |
| 208 weak_ptr_factory_.GetWeakPtr(), base::Passed(&handlers))); | |
| 209 } | |
| 210 | |
| 211 void ArcNavigationThrottle::OnAppIconsReceived( | |
| 212 mojo::Array<mojom::IntentHandlerInfoPtr> handlers, | |
| 213 std::unique_ptr<ActivityIconLoader::ActivityToIconsMap> icons) { | |
| 214 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 215 std::vector<NameAndIcon> app_info; | |
| 216 | |
| 217 for (const auto& handler : handlers) { | |
| 218 gfx::Image icon; | |
| 219 const ActivityIconLoader::ActivityName activity(handler->package_name, | |
| 220 handler->activity_name); | |
| 221 const auto it = icons->find(activity); | |
| 222 | |
| 223 app_info.emplace_back( | |
| 224 handler->name, it != icons->end() ? it->second.icon20 : gfx::Image()); | |
| 225 } | |
| 226 | |
| 227 show_intent_picker_callback_.Run( | |
| 228 navigation_handle()->GetWebContents(), app_info, | |
| 229 base::Bind(&ArcNavigationThrottle::OnIntentPickerClosed, | |
| 230 weak_ptr_factory_.GetWeakPtr(), base::Passed(&handlers))); | |
| 231 } | |
| 232 | |
| 233 void ArcNavigationThrottle::OnIntentPickerClosed( | |
| 234 mojo::Array<mojom::IntentHandlerInfoPtr> handlers, | |
| 235 size_t selected_app_index, | |
| 236 CloseReason close_reason) { | |
| 237 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 238 const GURL& url = navigation_handle()->GetURL(); | |
| 239 content::NavigationHandle* handle = navigation_handle(); | |
| 240 | |
| 241 previous_user_action_ = close_reason; | |
| 242 | |
| 243 // Make sure that the instance at least supports HandleUrl. | |
| 244 auto* instance = ArcIntentHelperBridge::GetIntentHelperInstance( | |
| 245 "HandleUrl", kMinVersionForHandleUrl); | |
| 246 if (!instance || selected_app_index >= handlers.size()) | |
| 247 close_reason = CloseReason::ERROR; | |
| 248 | |
| 249 switch (close_reason) { | |
| 250 case CloseReason::ERROR: | |
| 251 case CloseReason::DIALOG_DEACTIVATED: { | |
| 252 // If the user fails to select an option from the list, or the UI returned | |
| 253 // an error or if |selected_app_index| is not a valid index, then resume | |
| 254 // the navigation in Chrome. | |
| 255 DVLOG(1) << "User didn't select a valid option, resuming navigation."; | |
| 256 handle->Resume(); | |
| 257 break; | |
| 258 } | |
| 259 case CloseReason::ALWAYS_PRESSED: { | |
| 260 // Call AddPreferredPackage if it is supported. Reusing the same | |
| 261 // |instance| is okay. | |
| 262 if (ArcIntentHelperBridge::GetIntentHelperInstance( | |
| 263 "AddPreferredPackage", kMinVersionForAddPreferredPackage)) { | |
| 264 instance->AddPreferredPackage( | |
| 265 handlers[selected_app_index]->package_name); | |
| 266 } | |
| 267 // fall through. | |
| 268 } | |
| 269 case CloseReason::JUST_ONCE_PRESSED: | |
| 270 case CloseReason::PREFERRED_ACTIVITY_FOUND: { | |
| 271 if (ArcIntentHelperBridge::IsIntentHelperPackage( | |
| 272 handlers[selected_app_index]->package_name)) { | |
| 273 handle->Resume(); | |
| 274 } else { | |
| 275 instance->HandleUrl(url.spec(), | |
| 276 handlers[selected_app_index]->package_name); | |
| 277 handle->CancelDeferredNavigation( | |
| 278 content::NavigationThrottle::CANCEL_AND_IGNORE); | |
| 279 if (handle->GetWebContents()->GetController().IsInitialNavigation()) | |
| 280 handle->GetWebContents()->Close(); | |
| 281 } | |
| 282 break; | |
| 283 } | |
| 284 case CloseReason::INVALID: { | |
| 285 NOTREACHED(); | |
| 286 return; | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 UMA_HISTOGRAM_ENUMERATION("Arc.IntentHandlerAction", | |
| 291 static_cast<int>(close_reason), | |
| 292 static_cast<int>(CloseReason::SIZE)); | |
| 293 } | |
| 294 | |
| 295 // static | |
| 296 bool ArcNavigationThrottle::ShouldOverrideUrlLoadingForTesting( | |
| 297 const GURL& previous_url, | |
| 298 const GURL& current_url) { | |
| 299 return ShouldOverrideUrlLoading(previous_url, current_url); | |
| 300 } | |
| 301 | |
| 302 } // namespace arc | |
| OLD | NEW |