| // Copyright 2012 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. |
| |
| #include "chrome/browser/geolocation/geolocation_permission_context_android.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "chrome/browser/android/location_settings.h" |
| #include "chrome/browser/android/location_settings_impl.h" |
| #include "chrome/browser/android/search_permissions/search_geolocation_disclosure_tab_helper.h" |
| #include "chrome/browser/android/tab_android.h" |
| #include "chrome/browser/permissions/permission_request_id.h" |
| #include "chrome/browser/permissions/permission_uma_util.h" |
| #include "chrome/browser/permissions/permission_update_infobar_delegate_android.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/infobars/core/infobar.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/search_engines/template_url.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| int g_day_offset_for_testing = 0; |
| const char* g_dse_origin_for_testing = nullptr; |
| |
| const char kLocationSettingsShowMetricBase[] = |
| "Geolocation.SettingsDialog.ShowEvent."; |
| const char kLocationSettingsSuppressMetricBase[] = |
| "Geolocation.SettingsDialog.SuppressEvent."; |
| const char kLocationSettingsAcceptMetricBase[] = |
| "Geolocation.SettingsDialog.AcceptEvent."; |
| const char kLocationSettingsDenyMetricBase[] = |
| "Geolocation.SettingsDialog.DenyEvent."; |
| const char kLocationSettingsAcceptBatteryMetric[] = |
| "Permissions.BatteryLevel.Accepted.LocationSettingsDialog"; |
| const char kLocationSettingsDenyBatteryMetric[] = |
| "Permissions.BatteryLevel.Denied.LocationSettingsDialog"; |
| |
| const char kLocationSettingsMetricDSESuffix[] = "DSE"; |
| const char kLocationSettingsMetricNonDSESuffix[] = "NonDSE"; |
| |
| base::Time GetTimeNow() { |
| return base::Time::Now() + |
| base::TimeDelta::FromDays(g_day_offset_for_testing); |
| } |
| |
| void LogLocationSettingsMetric( |
| const std::string& metric_base, |
| bool is_default_search, |
| GeolocationPermissionContextAndroid::LocationSettingsDialogBackOff |
| backoff) { |
| std::string metric_name = metric_base; |
| if (is_default_search) |
| metric_name.append(kLocationSettingsMetricDSESuffix); |
| else |
| metric_name.append(kLocationSettingsMetricNonDSESuffix); |
| |
| base::UmaHistogramEnumeration(metric_name, backoff, |
| GeolocationPermissionContextAndroid:: |
| LocationSettingsDialogBackOff::kCount); |
| } |
| |
| } // namespace |
| |
| // static |
| void GeolocationPermissionContextAndroid::RegisterProfilePrefs( |
| PrefRegistrySimple* registry) { |
| registry->RegisterIntegerPref(prefs::kLocationSettingsBackoffLevelDSE, 0); |
| registry->RegisterIntegerPref(prefs::kLocationSettingsBackoffLevelDefault, 0); |
| registry->RegisterInt64Pref(prefs::kLocationSettingsNextShowDSE, 0); |
| registry->RegisterInt64Pref(prefs::kLocationSettingsNextShowDefault, 0); |
| } |
| |
| GeolocationPermissionContextAndroid::GeolocationPermissionContextAndroid( |
| Profile* profile) |
| : GeolocationPermissionContext(profile), |
| location_settings_(new LocationSettingsImpl()), |
| permission_update_infobar_(nullptr), |
| location_settings_dialog_request_id_(0, 0, 0), |
| weak_factory_(this) {} |
| |
| GeolocationPermissionContextAndroid::~GeolocationPermissionContextAndroid() { |
| } |
| |
| // static |
| void GeolocationPermissionContextAndroid::AddDayOffsetForTesting(int days) { |
| g_day_offset_for_testing += days; |
| } |
| |
| // static |
| void GeolocationPermissionContextAndroid::SetDSEOriginForTesting( |
| const char* dse_origin) { |
| g_dse_origin_for_testing = dse_origin; |
| } |
| |
| void GeolocationPermissionContextAndroid::RequestPermission( |
| content::WebContents* web_contents, |
| const PermissionRequestID& id, |
| const GURL& requesting_frame_origin, |
| bool user_gesture, |
| const BrowserPermissionCallback& callback) { |
| if (!IsLocationAccessPossible(web_contents, requesting_frame_origin, |
| user_gesture)) { |
| NotifyPermissionSet(id, requesting_frame_origin, |
| web_contents->GetLastCommittedURL().GetOrigin(), |
| callback, false /* persist */, CONTENT_SETTING_BLOCK); |
| return; |
| } |
| |
| GURL embedding_origin = web_contents->GetLastCommittedURL().GetOrigin(); |
| ContentSetting content_setting = |
| GeolocationPermissionContext::GetPermissionStatus( |
| nullptr /* render_frame_host */, requesting_frame_origin, |
| embedding_origin) |
| .content_setting; |
| std::vector<ContentSettingsType> content_settings_types; |
| content_settings_types.push_back(CONTENT_SETTINGS_TYPE_GEOLOCATION); |
| if (content_setting == CONTENT_SETTING_ALLOW && |
| PermissionUpdateInfoBarDelegate::ShouldShowPermissionInfobar( |
| web_contents, content_settings_types)) { |
| permission_update_infobar_ = PermissionUpdateInfoBarDelegate::Create( |
| web_contents, content_settings_types, |
| base::Bind( |
| &GeolocationPermissionContextAndroid |
| ::HandleUpdateAndroidPermissions, |
| weak_factory_.GetWeakPtr(), id, requesting_frame_origin, |
| embedding_origin, callback)); |
| |
| return; |
| } |
| |
| GeolocationPermissionContext::RequestPermission( |
| web_contents, id, requesting_frame_origin, user_gesture, callback); |
| } |
| |
| void GeolocationPermissionContextAndroid::CancelPermissionRequest( |
| content::WebContents* web_contents, |
| const PermissionRequestID& id) { |
| // TODO(timloh): This could cancel a infobar from an unrelated request. |
| if (permission_update_infobar_) { |
| permission_update_infobar_->RemoveSelf(); |
| permission_update_infobar_ = nullptr; |
| } |
| |
| if (id == location_settings_dialog_request_id_) { |
| location_settings_dialog_request_id_ = PermissionRequestID(0, 0, 0); |
| location_settings_dialog_callback_.Reset(); |
| } |
| |
| GeolocationPermissionContext::CancelPermissionRequest(web_contents, id); |
| } |
| |
| void GeolocationPermissionContextAndroid::UserMadePermissionDecision( |
| const PermissionRequestID& id, |
| const GURL& requesting_origin, |
| const GURL& embedding_origin, |
| ContentSetting content_setting) { |
| // If the user has accepted geolocation, reset the location settings dialog |
| // backoff. |
| if (content_setting == CONTENT_SETTING_ALLOW) |
| ResetLocationSettingsBackOff(IsRequestingOriginDSE(requesting_origin)); |
| } |
| |
| void GeolocationPermissionContextAndroid::NotifyPermissionSet( |
| const PermissionRequestID& id, |
| const GURL& requesting_origin, |
| const GURL& embedding_origin, |
| const BrowserPermissionCallback& callback, |
| bool persist, |
| ContentSetting content_setting) { |
| bool is_default_search = IsRequestingOriginDSE(requesting_origin); |
| if (content_setting == CONTENT_SETTING_ALLOW && |
| !location_settings_->IsSystemLocationSettingEnabled()) { |
| // There is no need to check CanShowLocationSettingsDialog here again, as it |
| // must have been checked to get this far. But, the backoff will not have |
| // been checked, so check that. Backoff isn't checked earlier because if the |
| // content setting is ASK the backoff should be ignored. However if we get |
| // here and the content setting was ASK, the user must have accepted which |
| // would reset the backoff. |
| if (IsInLocationSettingsBackOff(is_default_search)) { |
| FinishNotifyPermissionSet(id, requesting_origin, embedding_origin, |
| callback, false /* persist */, |
| CONTENT_SETTING_BLOCK); |
| LogLocationSettingsMetric( |
| kLocationSettingsSuppressMetricBase, is_default_search, |
| LocationSettingsBackOffLevel(is_default_search)); |
| return; |
| } |
| |
| LogLocationSettingsMetric(kLocationSettingsShowMetricBase, |
| is_default_search, |
| LocationSettingsBackOffLevel(is_default_search)); |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(id.render_process_id(), |
| id.render_frame_id())); |
| |
| // Only show the location settings dialog if the tab for |web_contents| is |
| // user-interactable (i.e. is the current tab, and Chrome is active and not |
| // in tab-switching mode) and we're not already showing the LSD. The latter |
| // case can occur in split-screen multi-window. |
| TabAndroid* tab = TabAndroid::FromWebContents(web_contents); |
| if ((tab && !tab->IsUserInteractable()) || |
| !location_settings_dialog_callback_.is_null()) { |
| FinishNotifyPermissionSet(id, requesting_origin, embedding_origin, |
| callback, false /* persist */, |
| CONTENT_SETTING_BLOCK); |
| // This case should be very rare, so just pretend it was a denied prompt |
| // for metrics purposes. |
| LogLocationSettingsMetric( |
| kLocationSettingsDenyMetricBase, is_default_search, |
| LocationSettingsBackOffLevel(is_default_search)); |
| return; |
| } |
| |
| location_settings_dialog_request_id_ = id; |
| location_settings_dialog_callback_ = callback; |
| location_settings_->PromptToEnableSystemLocationSetting( |
| is_default_search ? SEARCH : DEFAULT, web_contents, |
| base::BindOnce( |
| &GeolocationPermissionContextAndroid::OnLocationSettingsDialogShown, |
| weak_factory_.GetWeakPtr(), requesting_origin, embedding_origin, |
| persist, content_setting)); |
| return; |
| } |
| |
| FinishNotifyPermissionSet(id, requesting_origin, embedding_origin, callback, |
| persist, content_setting); |
| } |
| |
| PermissionResult |
| GeolocationPermissionContextAndroid::UpdatePermissionStatusWithDeviceStatus( |
| PermissionResult result, |
| const GURL& requesting_origin, |
| const GURL& embedding_origin) const { |
| if (base::FeatureList::IsEnabled(features::kLsdPermissionPrompt) && |
| result.content_setting != CONTENT_SETTING_BLOCK) { |
| if (!location_settings_->IsSystemLocationSettingEnabled()) { |
| // As this is returning the status for possible future permission |
| // requests, whose gesture status is unknown, pretend there is a user |
| // gesture here. If there is a possibility of PROMPT (i.e. if there is a |
| // user gesture attached to the later request) that should be returned, |
| // not BLOCK. |
| // If the permission is in the ASK state, backoff is ignored. Permission |
| // prompts are shown regardless of backoff, if the location is off and the |
| // LSD can be shown, as permission prompts are less annoying than the |
| // modal LSD, and if the user accepts the permission prompt the LSD |
| // backoff will be reset. |
| if (CanShowLocationSettingsDialog( |
| requesting_origin, true /* user_gesture */, |
| result.content_setting == |
| CONTENT_SETTING_ASK /* ignore_backoff */)) { |
| result.content_setting = CONTENT_SETTING_ASK; |
| } else { |
| result.content_setting = CONTENT_SETTING_BLOCK; |
| } |
| result.source = PermissionStatusSource::UNSPECIFIED; |
| } |
| |
| if (result.content_setting != CONTENT_SETTING_BLOCK && |
| !location_settings_->HasAndroidLocationPermission()) { |
| // TODO(benwells): plumb through the RFH and use the associated |
| // WebContents to check that the android location can be prompted for. |
| result.content_setting = CONTENT_SETTING_ASK; |
| result.source = PermissionStatusSource::UNSPECIFIED; |
| } |
| } |
| |
| return result; |
| } |
| |
| std::string |
| GeolocationPermissionContextAndroid::GetLocationSettingsBackOffLevelPref( |
| bool is_default_search) const { |
| return is_default_search ? prefs::kLocationSettingsBackoffLevelDSE |
| : prefs::kLocationSettingsBackoffLevelDefault; |
| } |
| |
| std::string |
| GeolocationPermissionContextAndroid::GetLocationSettingsNextShowPref( |
| bool is_default_search) const { |
| return is_default_search ? prefs::kLocationSettingsNextShowDSE |
| : prefs::kLocationSettingsNextShowDefault; |
| } |
| |
| bool GeolocationPermissionContextAndroid::IsInLocationSettingsBackOff( |
| bool is_default_search) const { |
| base::Time next_show = |
| base::Time::FromInternalValue(profile()->GetPrefs()->GetInt64( |
| GetLocationSettingsNextShowPref(is_default_search))); |
| |
| return GetTimeNow() < next_show; |
| } |
| |
| void GeolocationPermissionContextAndroid::ResetLocationSettingsBackOff( |
| bool is_default_search) { |
| PrefService* prefs = profile()->GetPrefs(); |
| prefs->ClearPref(GetLocationSettingsNextShowPref(is_default_search)); |
| prefs->ClearPref(GetLocationSettingsBackOffLevelPref(is_default_search)); |
| } |
| |
| void GeolocationPermissionContextAndroid::UpdateLocationSettingsBackOff( |
| bool is_default_search) { |
| LocationSettingsDialogBackOff backoff_level = |
| LocationSettingsBackOffLevel(is_default_search); |
| |
| base::Time next_show = GetTimeNow(); |
| switch (backoff_level) { |
| case LocationSettingsDialogBackOff::kNoBackOff: |
| backoff_level = LocationSettingsDialogBackOff::kOneWeek; |
| next_show += base::TimeDelta::FromDays(7); |
| break; |
| case LocationSettingsDialogBackOff::kOneWeek: |
| backoff_level = LocationSettingsDialogBackOff::kOneMonth; |
| next_show += base::TimeDelta::FromDays(30); |
| break; |
| case LocationSettingsDialogBackOff::kOneMonth: |
| backoff_level = LocationSettingsDialogBackOff::kThreeMonths; |
| // fall through |
| case LocationSettingsDialogBackOff::kThreeMonths: |
| next_show += base::TimeDelta::FromDays(90); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| PrefService* prefs = profile()->GetPrefs(); |
| prefs->SetInteger(GetLocationSettingsBackOffLevelPref(is_default_search), |
| static_cast<int>(backoff_level)); |
| prefs->SetInt64(GetLocationSettingsNextShowPref(is_default_search), |
| next_show.ToInternalValue()); |
| } |
| |
| GeolocationPermissionContextAndroid::LocationSettingsDialogBackOff |
| GeolocationPermissionContextAndroid::LocationSettingsBackOffLevel( |
| bool is_default_search) const { |
| PrefService* prefs = profile()->GetPrefs(); |
| int int_backoff = |
| prefs->GetInteger(GetLocationSettingsBackOffLevelPref(is_default_search)); |
| return static_cast<LocationSettingsDialogBackOff>(int_backoff); |
| } |
| |
| bool GeolocationPermissionContextAndroid::IsLocationAccessPossible( |
| content::WebContents* web_contents, |
| const GURL& requesting_origin, |
| bool user_gesture) { |
| return (location_settings_->HasAndroidLocationPermission() || |
| location_settings_->CanPromptForAndroidLocationPermission( |
| web_contents)) && |
| (location_settings_->IsSystemLocationSettingEnabled() || |
| CanShowLocationSettingsDialog(requesting_origin, user_gesture, |
| true /* ignore_backoff */)); |
| } |
| |
| bool GeolocationPermissionContextAndroid::IsRequestingOriginDSE( |
| const GURL& requesting_origin) const { |
| GURL dse_url; |
| |
| if (g_dse_origin_for_testing) { |
| dse_url = GURL(g_dse_origin_for_testing); |
| } else { |
| TemplateURLService* template_url_service = |
| TemplateURLServiceFactory::GetForProfile(profile()); |
| if (template_url_service) { |
| const TemplateURL* template_url = |
| template_url_service->GetDefaultSearchProvider(); |
| if (template_url) { |
| dse_url = template_url->GenerateSearchURL( |
| template_url_service->search_terms_data()); |
| } |
| } |
| } |
| |
| return url::IsSameOriginWith(requesting_origin, dse_url); |
| } |
| |
| void GeolocationPermissionContextAndroid::HandleUpdateAndroidPermissions( |
| const PermissionRequestID& id, |
| const GURL& requesting_frame_origin, |
| const GURL& embedding_origin, |
| const BrowserPermissionCallback& callback, |
| bool permissions_updated) { |
| permission_update_infobar_ = nullptr; |
| |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| ContentSetting new_setting = permissions_updated |
| ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK; |
| |
| NotifyPermissionSet(id, requesting_frame_origin, embedding_origin, callback, |
| false /* persist */, new_setting); |
| } |
| |
| bool GeolocationPermissionContextAndroid::CanShowLocationSettingsDialog( |
| const GURL& requesting_origin, |
| bool user_gesture, |
| bool ignore_backoff) const { |
| if (!base::FeatureList::IsEnabled(features::kLsdPermissionPrompt)) |
| return false; |
| |
| bool is_default_search = IsRequestingOriginDSE(requesting_origin); |
| // If this isn't the default search engine, a gesture is needed. |
| if (!is_default_search && !user_gesture) { |
| return false; |
| } |
| |
| if (!ignore_backoff && IsInLocationSettingsBackOff(is_default_search)) |
| return false; |
| |
| return location_settings_->CanPromptToEnableSystemLocationSetting(); |
| } |
| |
| void GeolocationPermissionContextAndroid::OnLocationSettingsDialogShown( |
| const GURL& requesting_origin, |
| const GURL& embedding_origin, |
| bool persist, |
| ContentSetting content_setting, |
| LocationSettingsDialogOutcome prompt_outcome) { |
| bool is_default_search = IsRequestingOriginDSE(requesting_origin); |
| if (prompt_outcome == GRANTED) { |
| LogLocationSettingsMetric(kLocationSettingsAcceptMetricBase, |
| is_default_search, |
| LocationSettingsBackOffLevel(is_default_search)); |
| PermissionUmaUtil::RecordWithBatteryBucket( |
| kLocationSettingsAcceptBatteryMetric); |
| ResetLocationSettingsBackOff(is_default_search); |
| } else { |
| LogLocationSettingsMetric(kLocationSettingsDenyMetricBase, |
| is_default_search, |
| LocationSettingsBackOffLevel(is_default_search)); |
| PermissionUmaUtil::RecordWithBatteryBucket( |
| kLocationSettingsDenyBatteryMetric); |
| UpdateLocationSettingsBackOff(is_default_search); |
| content_setting = CONTENT_SETTING_BLOCK; |
| persist = false; |
| } |
| |
| // If the permission was cancelled while the LSD was up, the callback has |
| // already been dropped. |
| if (location_settings_dialog_callback_.is_null()) |
| return; |
| |
| FinishNotifyPermissionSet( |
| location_settings_dialog_request_id_, requesting_origin, embedding_origin, |
| location_settings_dialog_callback_, persist, content_setting); |
| |
| location_settings_dialog_request_id_ = PermissionRequestID(0, 0, 0); |
| location_settings_dialog_callback_.Reset(); |
| } |
| |
| void GeolocationPermissionContextAndroid::FinishNotifyPermissionSet( |
| const PermissionRequestID& id, |
| const GURL& requesting_origin, |
| const GURL& embedding_origin, |
| const BrowserPermissionCallback& callback, |
| bool persist, |
| ContentSetting content_setting) { |
| GeolocationPermissionContext::NotifyPermissionSet(id, requesting_origin, |
| embedding_origin, callback, |
| persist, content_setting); |
| |
| // If this is the default search origin, and the DSE Geolocation setting is |
| // being used, potentially show the disclosure. |
| if (requesting_origin == embedding_origin) { |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(id.render_process_id(), |
| id.render_frame_id())); |
| if (!web_contents) |
| return; |
| |
| SearchGeolocationDisclosureTabHelper* disclosure_helper = |
| SearchGeolocationDisclosureTabHelper::FromWebContents(web_contents); |
| |
| // The tab helper can be null in tests. |
| if (disclosure_helper) |
| disclosure_helper->MaybeShowDisclosureForAPIAccess(requesting_origin); |
| } |
| } |
| |
| void GeolocationPermissionContextAndroid::SetLocationSettingsForTesting( |
| std::unique_ptr<LocationSettings> settings) { |
| location_settings_ = std::move(settings); |
| } |