| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/geolocation/omnibox_geolocation_controller.h" |
| |
| #import <CoreLocation/CoreLocation.h> |
| #import <UIKit/UIKit.h> |
| |
| #include <string> |
| |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/version.h" |
| #include "components/google/core/browser/google_util.h" |
| #include "components/version_info/version_info.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #import "ios/chrome/browser/geolocation/CLLocation+OmniboxGeolocation.h" |
| #import "ios/chrome/browser/geolocation/CLLocation+XGeoHeader.h" |
| #import "ios/chrome/browser/geolocation/location_manager.h" |
| #import "ios/chrome/browser/geolocation/omnibox_geolocation_authorization_alert.h" |
| #import "ios/chrome/browser/geolocation/omnibox_geolocation_config.h" |
| #import "ios/chrome/browser/geolocation/omnibox_geolocation_controller+Testing.h" |
| #import "ios/chrome/browser/geolocation/omnibox_geolocation_local_state.h" |
| #import "ios/chrome/browser/tabs/tab.h" |
| #include "ios/web/public/navigation_item.h" |
| #import "ios/web/public/navigation_manager.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| // Values for the histogram that records whether we sent the X-Geo header for |
| // an Omnibox query or why we did not do so. These match the definition of |
| // GeolocationHeaderSentOrNot in Chromium |
| // src-internal/tools/histograms/histograms.xml. |
| typedef enum { |
| // The user disabled location for Google.com (not used by Chrome iOS). |
| kHeaderStateNotSentAuthorizationGoogleDenied = 0, |
| // The user has not yet determined Chrome's access to the current device |
| // location or Chrome's use of geolocation for Omnibox queries. |
| kHeaderStateNotSentAuthorizationNotDetermined, |
| // The current device location is not available. |
| kHeaderStateNotSentLocationNotAvailable, |
| // The current device location is stale. |
| kHeaderStateNotSentLocationStale, |
| // The X-Geo header was sent. |
| kHeaderStateSent, |
| // The user denied Chrome from accessing the current device location. |
| kHeaderStateNotSentAuthorizationChromeDenied, |
| // The user denied Chrome from using geolocation for Omnibox queries. |
| kHeaderStateNotSentAuthorizationOmniboxDenied, |
| // The user's Google search domain is not whitelisted. |
| kHeaderStateNotSentDomainNotWhitelisted, |
| // The number of possible of HeaderState values to report. |
| kHeaderStateCount, |
| } HeaderState; |
| |
| // Values for the histograms that record the user's action when prompted to |
| // authorize the use of location by Chrome. These match the definition of |
| // GeolocationAuthorizationAction in Chromium |
| // src-internal/tools/histograms/histograms.xml. |
| typedef enum { |
| // The user authorized use of location. |
| kAuthorizationActionAuthorized = 0, |
| // The user permanently denied use of location (Don't Allow). |
| kAuthorizationActionPermanentlyDenied, |
| // The user denied use of location at this prompt (Not Now). |
| kAuthorizationActionDenied, |
| // The number of possible AuthorizationAction values to report. |
| kAuthorizationActionCount, |
| } AuthorizationAction; |
| |
| // Name of the histogram recording HeaderState. |
| const char* const kGeolocationHeaderSentOrNotHistogram = |
| "Geolocation.HeaderSentOrNot"; |
| |
| // Name of the histogram recording location acquisition time. |
| const char* const kOmniboxQueryGeolocationAcquisitionTimeHistogram = |
| "Omnibox.QueryGeolocationAcquisitionTime"; |
| |
| // Name of the histogram recording estimated location accuracy. |
| const char* const kOmniboxQueryGeolocationHorizontalAccuracyHistogram = |
| "Omnibox.QueryGeolocationHorizontalAccuracy"; |
| |
| // Name of the histogram recording AuthorizationAction for an existing user. |
| const char* const kGeolocationAuthorizationActionExistingUser = |
| "Geolocation.AuthorizationActionExistingUser"; |
| |
| // Name of the histogram recording AuthorizationAction for a new user. |
| const char* const kGeolocationAuthorizationActionNewUser = |
| "Geolocation.AuthorizationActionNewUser"; |
| |
| } // anonymous namespace |
| |
| @interface OmniboxGeolocationController ()< |
| LocationManagerDelegate, |
| OmniboxGeolocationAuthorizationAlertDelegate> { |
| OmniboxGeolocationLocalState* localState_; |
| LocationManager* locationManager_; |
| OmniboxGeolocationAuthorizationAlert* authorizationAlert_; |
| __weak Tab* weakTabToReload_; |
| |
| // Records whether we have deliberately presented the system prompt, so that |
| // we can record the user's action in |
| // locationManagerDidChangeAuthorizationStatus:. |
| BOOL systemPrompt_; |
| |
| // Records whether we are prompting for a new user, so that we can record the |
| // user's action to the right histogram (either |
| // kGeolocationAuthorizationActionExistingUser or |
| // kGeolocationAuthorizationActionNewUser). |
| BOOL newUser_; |
| } |
| |
| // Boolean value indicating whether geolocation is enabled for Omnibox queries. |
| @property(nonatomic, readonly) BOOL enabled; |
| |
| // Convenience property lazily initializes |localState_|. |
| @property(nonatomic, readonly) OmniboxGeolocationLocalState* localState; |
| |
| // Convenience property lazily initializes |locationManager_|. |
| @property(nonatomic, readonly) LocationManager* locationManager; |
| |
| // Returns YES if and only if |url| and |transition| specify an Omnibox query |
| // that is eligible for geolocation. |
| - (BOOL)URLIsEligibleQueryURL:(const GURL&)url |
| transition:(ui::PageTransition)transition; |
| |
| // Returns YES if and only if |url| and |transition| specify an Omnibox query. |
| // |
| // Note: URLIsQueryURL:transition: is more liberal than |
| // URLIsEligibleQueryURL:transition:. Use URLIsEligibleQueryURL:transition: and |
| // not URLIsQueryURL:transition: to test Omnibox query URLs with respect to |
| // sending location to Google. |
| - (BOOL)URLIsQueryURL:(const GURL&)url |
| transition:(ui::PageTransition)transition; |
| |
| // Returns YES if and only if |url| specifies a page for which we will prompt |
| // the user to authorize the use of geolocation for Omnibox queries. |
| - (BOOL)URLIsAuthorizationPromptingURL:(const GURL&)url; |
| |
| // Starts updating device location if needed. |
| - (void)startUpdatingLocation; |
| // Stops updating device location. |
| - (void)stopUpdatingLocation; |
| // If the current location is not stale, then adds the current location to the |
| // current session entry for |tab| and reloads |tab|. If the current location |
| // is stale, then does nothing. |
| - (void)addLocationAndReloadTab:(Tab*)tab; |
| // Returns YES if and only if we should show an alert that prompts the user to |
| // authorize using geolocation for Omnibox queries. |
| - (BOOL)shouldShowAuthorizationAlert; |
| // Shows an alert that prompts the user to authorize using geolocation for |
| // Omnibox queries. Sets |weakTabToReload_| from |tab|, so that we can reload |
| // |tab| if the user authorizes using geolocation. |
| - (void)showAuthorizationAlertForTab:(Tab*)tab; |
| // Records |headerState| for the |kGeolocationHeaderSentOrNotHistogram| |
| // histogram. |
| - (void)recordHeaderState:(HeaderState)headerState; |
| // Records |authorizationAction|. |
| - (void)recordAuthorizationAction:(AuthorizationAction)authorizationAction; |
| |
| @end |
| |
| @implementation OmniboxGeolocationController |
| |
| + (OmniboxGeolocationController*)sharedInstance { |
| static OmniboxGeolocationController* instance = |
| [[OmniboxGeolocationController alloc] init]; |
| return instance; |
| } |
| |
| - (void)triggerSystemPromptForNewUser:(BOOL)newUser { |
| if (self.locationManager.locationServicesEnabled && |
| self.locationManager.authorizationStatus == |
| kCLAuthorizationStatusNotDetermined) { |
| // Set |systemPrompt_|, so that |
| // locationManagerDidChangeAuthorizationStatus: will know to handle any |
| // CLAuthorizationStatus changes. |
| // |
| // TODO(crbug.com/661996): Remove the now useless |
| // kAuthorizationStateNotDeterminedSystemPrompt from |
| // omnibox_geolocation_local_state.h. |
| systemPrompt_ = YES; |
| self.localState.authorizationState = |
| geolocation::kAuthorizationStateNotDeterminedSystemPrompt; |
| |
| // Turn on location updates, so that iOS will prompt the user. |
| [self startUpdatingLocation]; |
| |
| weakTabToReload_ = nil; |
| newUser_ = newUser; |
| } |
| } |
| |
| - (void)locationBarDidBecomeFirstResponder: |
| (ios::ChromeBrowserState*)browserState { |
| if (self.enabled && browserState && !browserState->IsOffTheRecord()) { |
| [self startUpdatingLocation]; |
| } |
| } |
| |
| - (void)locationBarDidResignFirstResponder: |
| (ios::ChromeBrowserState*)browserState { |
| // It's always okay to stop updating location. |
| [self stopUpdatingLocation]; |
| } |
| |
| - (void)locationBarDidSubmitURL:(const GURL&)url |
| transition:(ui::PageTransition)transition |
| browserState:(ios::ChromeBrowserState*)browserState { |
| // Stop updating the location when the user submits a query from the Omnibox. |
| // We're not interested in further updates until the next time the user puts |
| // the focus on the Omnbox. |
| [self stopUpdatingLocation]; |
| } |
| |
| - (BOOL)addLocationToNavigationItem:(web::NavigationItem*)item |
| browserState:(ios::ChromeBrowserState*)browserState { |
| // If this is incognito mode or is not an Omnibox query, then do nothing. |
| // |
| // Check the URL with URLIsQueryURL:transition: here and not |
| // URLIsEligibleQueryURL:transition:, because we want to log the cases where |
| // we did not send the X-Geo header due to the Google search domain not being |
| // whitelisted. |
| DCHECK(item); |
| const GURL& url = item->GetURL(); |
| if (!browserState || browserState->IsOffTheRecord() || |
| ![self URLIsQueryURL:url transition:item->GetTransitionType()]) { |
| return NO; |
| } |
| |
| if (![[OmniboxGeolocationConfig sharedInstance] URLHasEligibleDomain:url]) { |
| [self recordHeaderState:kHeaderStateNotSentDomainNotWhitelisted]; |
| return NO; |
| } |
| |
| // At this point, we should only have Omnibox query URLs that are eligible |
| // for geolocation. |
| DCHECK([self URLIsEligibleQueryURL:url transition:item->GetTransitionType()]); |
| |
| HeaderState headerState; |
| if (!self.locationManager.locationServicesEnabled) { |
| headerState = kHeaderStateNotSentAuthorizationChromeDenied; |
| } else { |
| switch (self.localState.authorizationState) { |
| case geolocation::kAuthorizationStateNotDeterminedWaiting: |
| case geolocation::kAuthorizationStateNotDeterminedSystemPrompt: |
| if (self.locationManager.authorizationStatus == |
| kCLAuthorizationStatusNotDetermined || |
| [self shouldShowAuthorizationAlert]) { |
| headerState = kHeaderStateNotSentAuthorizationNotDetermined; |
| } else { |
| DCHECK(self.locationManager.authorizationStatus == |
| kCLAuthorizationStatusAuthorizedAlways || |
| self.locationManager.authorizationStatus == |
| kCLAuthorizationStatusAuthorizedWhenInUse); |
| headerState = kHeaderStateNotSentAuthorizationOmniboxDenied; |
| } |
| break; |
| |
| case geolocation::kAuthorizationStateDenied: |
| switch (self.locationManager.authorizationStatus) { |
| case kCLAuthorizationStatusNotDetermined: |
| NOTREACHED(); |
| // To keep the compiler quiet about headerState not being |
| // initialized in this switch case. |
| headerState = kHeaderStateNotSentAuthorizationChromeDenied; |
| break; |
| case kCLAuthorizationStatusRestricted: |
| case kCLAuthorizationStatusDenied: |
| headerState = kHeaderStateNotSentAuthorizationChromeDenied; |
| break; |
| case kCLAuthorizationStatusAuthorizedAlways: |
| case kCLAuthorizationStatusAuthorizedWhenInUse: |
| headerState = kHeaderStateNotSentAuthorizationOmniboxDenied; |
| break; |
| } |
| break; |
| |
| case geolocation::kAuthorizationStateAuthorized: { |
| DCHECK(self.enabled); |
| CLLocation* currentLocation = [self.locationManager currentLocation]; |
| if (!currentLocation) { |
| headerState = kHeaderStateNotSentLocationNotAvailable; |
| } else if (![currentLocation cr_isFreshEnough]) { |
| headerState = kHeaderStateNotSentLocationStale; |
| } else { |
| NSDictionary* locationHTTPHeaders = |
| @{ @"X-Geo" : [currentLocation cr_xGeoString] }; |
| item->AddHttpRequestHeaders(locationHTTPHeaders); |
| headerState = kHeaderStateSent; |
| |
| NSTimeInterval acquisitionInterval = |
| currentLocation.cr_acquisitionInterval; |
| base::TimeDelta acquisitionTime = base::TimeDelta::FromMilliseconds( |
| acquisitionInterval * base::Time::kMillisecondsPerSecond); |
| UMA_HISTOGRAM_TIMES(kOmniboxQueryGeolocationAcquisitionTimeHistogram, |
| acquisitionTime); |
| |
| double horizontalAccuracy = currentLocation.horizontalAccuracy; |
| UMA_HISTOGRAM_COUNTS_10000( |
| kOmniboxQueryGeolocationHorizontalAccuracyHistogram, |
| horizontalAccuracy); |
| } |
| break; |
| } |
| } |
| } |
| |
| [self recordHeaderState:headerState]; |
| return headerState == kHeaderStateSent; |
| } |
| |
| - (void)finishPageLoadForTab:(Tab*)tab loadSuccess:(BOOL)loadSuccess { |
| if (tab.isPrerenderTab || !loadSuccess || !tab.browserState || |
| tab.browserState->IsOffTheRecord()) { |
| return; |
| } |
| |
| DCHECK(tab.webState->GetNavigationManager()); |
| web::NavigationItem* item = |
| tab.webState->GetNavigationManager()->GetVisibleItem(); |
| if (![self URLIsAuthorizationPromptingURL:item->GetURL()] || |
| !self.locationManager.locationServicesEnabled) { |
| return; |
| } |
| |
| switch (self.locationManager.authorizationStatus) { |
| case kCLAuthorizationStatusNotDetermined: |
| // Prompt the user with the iOS system location authorization alert. |
| // |
| // Set |systemPrompt_|, so that |
| // locationManagerDidChangeAuthorizationStatus: will know that any |
| // CLAuthorizationStatus changes are coming from this specific prompt. |
| systemPrompt_ = YES; |
| self.localState.authorizationState = |
| geolocation::kAuthorizationStateNotDeterminedSystemPrompt; |
| [self startUpdatingLocation]; |
| |
| // Save this tab in case we're able to transition to |
| // kAuthorizationStateAuthorized. |
| weakTabToReload_ = tab; |
| break; |
| |
| case kCLAuthorizationStatusRestricted: |
| case kCLAuthorizationStatusDenied: |
| break; |
| |
| case kCLAuthorizationStatusAuthorizedAlways: |
| case kCLAuthorizationStatusAuthorizedWhenInUse: |
| // We might be in state kAuthorizationStateNotDeterminedSystemPrompt here |
| // if we presented the iOS system location alert when |
| // [CLLocationManager authorizationStatus] was |
| // kCLAuthorizationStatusNotDetermined but the user managed to authorize |
| // the app through some other flow; this might happen if the user |
| // backgrounded the app or the app crashed. If so, then reset the state. |
| if (self.localState.authorizationState == |
| geolocation::kAuthorizationStateNotDeterminedSystemPrompt) { |
| self.localState.authorizationState = |
| geolocation::kAuthorizationStateNotDeterminedWaiting; |
| } |
| // If the user has authorized the app to use location but not yet |
| // explicitly authorized or denied using geolocation for Omnibox queries, |
| // then present an alert. |
| if (self.localState.authorizationState == |
| geolocation::kAuthorizationStateNotDeterminedWaiting && |
| [self shouldShowAuthorizationAlert]) { |
| [self showAuthorizationAlertForTab:tab]; |
| } |
| break; |
| } |
| } |
| |
| #pragma mark - Private |
| |
| - (BOOL)enabled { |
| return self.locationManager.locationServicesEnabled && |
| self.localState.authorizationState == |
| geolocation::kAuthorizationStateAuthorized; |
| } |
| |
| - (OmniboxGeolocationLocalState*)localState { |
| if (!localState_) { |
| localState_ = [[OmniboxGeolocationLocalState alloc] |
| initWithLocationManager:self.locationManager]; |
| } |
| return localState_; |
| } |
| |
| - (LocationManager*)locationManager { |
| if (!locationManager_) { |
| locationManager_ = [[LocationManager alloc] init]; |
| [locationManager_ setDelegate:self]; |
| } |
| return locationManager_; |
| } |
| |
| - (BOOL)URLIsEligibleQueryURL:(const GURL&)url |
| transition:(ui::PageTransition)transition { |
| return [self URLIsQueryURL:url transition:transition] && |
| [[OmniboxGeolocationConfig sharedInstance] URLHasEligibleDomain:url]; |
| } |
| |
| - (BOOL)URLIsQueryURL:(const GURL&)url |
| transition:(ui::PageTransition)transition { |
| if (google_util::IsGoogleSearchUrl(url) && |
| (transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) != 0) { |
| ui::PageTransition coreTransition = static_cast<ui::PageTransition>( |
| transition & ui::PAGE_TRANSITION_CORE_MASK); |
| if (PageTransitionCoreTypeIs(coreTransition, |
| ui::PAGE_TRANSITION_GENERATED) || |
| PageTransitionCoreTypeIs(coreTransition, ui::PAGE_TRANSITION_RELOAD)) { |
| return YES; |
| } |
| } |
| return NO; |
| } |
| |
| - (BOOL)URLIsAuthorizationPromptingURL:(const GURL&)url { |
| // Per PRD: "Show a modal dialog upon reaching google.com or a search results |
| // page..." However, we only want to do this for domains where we will send |
| // location. |
| return (google_util::IsGoogleHomePageUrl(url) || |
| google_util::IsGoogleSearchUrl(url)) && |
| [[OmniboxGeolocationConfig sharedInstance] URLHasEligibleDomain:url]; |
| } |
| |
| - (void)startUpdatingLocation { |
| // Note that GeolocationUpdater will stop itself automatically after 5 |
| // seconds. |
| [self.locationManager startUpdatingLocation]; |
| } |
| |
| - (void)stopUpdatingLocation { |
| // Note that we don't need to initialize |locationManager_| here. If it's |
| // nil, then it's not running. |
| [locationManager_ stopUpdatingLocation]; |
| } |
| |
| - (void)addLocationAndReloadTab:(Tab*)tab { |
| if (self.enabled && tab.webState) { |
| // Make sure that GeolocationUpdater is running the first time we request |
| // the current location. |
| // |
| // If GeolocationUpdater is not running, then it returns nil for the |
| // current location. That's normally okay, because we cache the most recent |
| // location in LocationManager. However, we arrive here when the user first |
| // authorizes us to use location, so we may not have ever started |
| // GeolocationUpdater. |
| [self startUpdatingLocation]; |
| |
| web::NavigationManager* navigationManager = |
| tab.webState->GetNavigationManager(); |
| web::NavigationItem* item = navigationManager->GetVisibleItem(); |
| if ([self addLocationToNavigationItem:item browserState:tab.browserState]) { |
| navigationManager->Reload(web::ReloadType::NORMAL, |
| false /* check_for_repost */); |
| } |
| } |
| } |
| |
| - (BOOL)shouldShowAuthorizationAlert { |
| base::Version previousVersion(self.localState.lastAuthorizationAlertVersion); |
| if (!previousVersion.IsValid()) |
| return YES; |
| |
| base::Version currentVersion(version_info::GetVersionNumber()); |
| DCHECK(currentVersion.IsValid()); |
| return currentVersion.components()[0] != previousVersion.components()[0]; |
| } |
| |
| - (void)showAuthorizationAlertForTab:(Tab*)tab { |
| // Save this tab in case we're able to transition to |
| // kAuthorizationStateAuthorized. |
| weakTabToReload_ = tab; |
| |
| authorizationAlert_ = |
| [[OmniboxGeolocationAuthorizationAlert alloc] initWithDelegate:self]; |
| [authorizationAlert_ showAuthorizationAlert]; |
| |
| self.localState.lastAuthorizationAlertVersion = |
| version_info::GetVersionNumber(); |
| } |
| |
| - (void)recordHeaderState:(HeaderState)headerState { |
| UMA_HISTOGRAM_ENUMERATION(kGeolocationHeaderSentOrNotHistogram, headerState, |
| kHeaderStateCount); |
| } |
| |
| - (void)recordAuthorizationAction:(AuthorizationAction)authorizationAction { |
| if (newUser_) { |
| newUser_ = NO; |
| |
| UMA_HISTOGRAM_ENUMERATION(kGeolocationAuthorizationActionNewUser, |
| authorizationAction, kAuthorizationActionCount); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION(kGeolocationAuthorizationActionExistingUser, |
| authorizationAction, kAuthorizationActionCount); |
| } |
| } |
| |
| #pragma mark - LocationManagerDelegate |
| |
| - (void)locationManagerDidChangeAuthorizationStatus: |
| (LocationManager*)locationManager { |
| if (systemPrompt_) { |
| switch (self.locationManager.authorizationStatus) { |
| case kCLAuthorizationStatusNotDetermined: |
| // We may get a spurious notification about a transition to |
| // |kCLAuthorizationStatusNotDetermined| when we first start location |
| // services. Ignore it and don't reset |systemPrompt_| until we get a |
| // real change. |
| break; |
| |
| case kCLAuthorizationStatusRestricted: |
| case kCLAuthorizationStatusDenied: |
| self.localState.authorizationState = |
| geolocation::kAuthorizationStateDenied; |
| systemPrompt_ = NO; |
| |
| [self recordAuthorizationAction:kAuthorizationActionPermanentlyDenied]; |
| break; |
| |
| case kCLAuthorizationStatusAuthorizedAlways: |
| case kCLAuthorizationStatusAuthorizedWhenInUse: |
| self.localState.authorizationState = |
| geolocation::kAuthorizationStateAuthorized; |
| systemPrompt_ = NO; |
| |
| Tab* tab = weakTabToReload_; |
| [self addLocationAndReloadTab:tab]; |
| weakTabToReload_ = nil; |
| |
| [self recordAuthorizationAction:kAuthorizationActionAuthorized]; |
| break; |
| } |
| } |
| } |
| |
| #pragma mark - OmniboxGeolocationAuthorizationAlertDelegate |
| |
| - (void)authorizationAlertDidAuthorize: |
| (OmniboxGeolocationAuthorizationAlert*)authorizationAlert { |
| self.localState.authorizationState = |
| geolocation::kAuthorizationStateAuthorized; |
| |
| Tab* tab = weakTabToReload_; |
| [self addLocationAndReloadTab:tab]; |
| |
| authorizationAlert_ = nil; |
| weakTabToReload_ = nil; |
| |
| [self recordAuthorizationAction:kAuthorizationActionAuthorized]; |
| } |
| |
| - (void)authorizationAlertDidCancel: |
| (OmniboxGeolocationAuthorizationAlert*)authorizationAlert { |
| // Leave authorization state as undetermined (not kAuthorizationStateDenied). |
| // We won't use location, but we'll still be able to prompt at the next |
| // application update. |
| |
| authorizationAlert_ = nil; |
| weakTabToReload_ = nil; |
| |
| [self recordAuthorizationAction:kAuthorizationActionDenied]; |
| } |
| |
| #pragma mark - OmniboxGeolocationController+Testing |
| |
| - (void)setLocalState:(OmniboxGeolocationLocalState*)localState { |
| localState_ = localState; |
| } |
| |
| - (void)setLocationManager:(LocationManager*)locationManager { |
| locationManager_ = locationManager; |
| } |
| |
| @end |