| // Copyright 2019 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/permissions/adaptive_quiet_notification_permission_ui_enabler.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/containers/adapters.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/stl_util.h" |
| #include "base/time/default_clock.h" |
| #include "base/util/values/values_util.h" |
| #include "base/values.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/permissions/quiet_notification_permission_ui_config.h" |
| #include "chrome/browser/permissions/quiet_notification_permission_ui_state.h" |
| #include "chrome/browser/profiles/incognito_helpers.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/pref_names.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "components/permissions/permission_util.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_change_registrar.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| |
| namespace { |
| |
| using EnablingMethod = QuietNotificationPermissionUiState::EnablingMethod; |
| |
| // Enable the quiet UX after 3 consecutive denies in adapative activation mode. |
| constexpr int kConsecutiveDeniesThresholdForActivation = 3u; |
| |
| // Inner structure of |prefs::kNotificationPermissionActions| containing a |
| // history of past permission actions. It is a JSON list with the format: |
| // |
| // "profile.content_settings.permission_actions.notifications": [ |
| // { "time": "1333333333337", "action": 1 }, |
| // { "time": "1567957177000", "action": 3 }, |
| // ... |
| // ] |
| // |
| constexpr char kPermissionActionEntryActionKey[] = "action"; |
| constexpr char kPermissionActionEntryTimestampKey[] = "time"; |
| |
| // Entries in permission actions expire after they become this old. |
| constexpr base::TimeDelta kPermissionActionMaxAge = |
| base::TimeDelta::FromDays(90); |
| |
| constexpr char kDidAdaptivelyEnableQuietUiInPrefs[] = |
| "Permissions.QuietNotificationPrompts.DidEnableAdapativelyInPrefs"; |
| constexpr char kIsQuietUiEnabledInPrefs[] = |
| "Permissions.QuietNotificationPrompts.IsEnabledInPrefs"; |
| constexpr char kQuietUiEnabledStateInPrefsChangedTo[] = |
| "Permissions.QuietNotificationPrompts.EnabledStateInPrefsChangedTo"; |
| |
| bool DidDenyLastThreeTimes(base::Value::ConstListView permission_actions, |
| base::Clock* clock_) { |
| // Limit how old actions are to be taken into consideration. Default 90 days |
| // old. The value could be changed based on Finch experiment but specifying > |
| // 90 days is meaningless, as only 90 days worth of history is stored. |
| const base::Time cutoff = |
| clock_->Now() - |
| QuietNotificationPermissionUiConfig::GetAdaptiveActivationWindowSize(); |
| size_t rolling_denies_in_a_row = 0u; |
| for (const auto& action : base::Reversed(permission_actions)) { |
| const base::Optional<base::Time> timestamp = |
| util::ValueToTime(action.FindKey(kPermissionActionEntryTimestampKey)); |
| |
| if (!timestamp || *timestamp < cutoff) |
| return false; |
| |
| const base::Optional<int> past_action_as_int = |
| action.FindIntKey(kPermissionActionEntryActionKey); |
| DCHECK(past_action_as_int); |
| |
| const permissions::PermissionAction past_action = |
| static_cast<permissions::PermissionAction>(*past_action_as_int); |
| |
| switch (past_action) { |
| case permissions::PermissionAction::DENIED: |
| ++rolling_denies_in_a_row; |
| break; |
| case permissions::PermissionAction::GRANTED: |
| return false; // Does not satisfy adaptive quiet UI activation |
| // condition. |
| case permissions::PermissionAction::DISMISSED: |
| case permissions::PermissionAction::IGNORED: |
| case permissions::PermissionAction::REVOKED: |
| default: |
| // Ignored. |
| break; |
| } |
| |
| if (rolling_denies_in_a_row >= kConsecutiveDeniesThresholdForActivation) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| // AdaptiveQuietNotificationPermissionUiEnabler::Factory --------------------- |
| |
| // static |
| AdaptiveQuietNotificationPermissionUiEnabler* |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory::GetForProfile( |
| Profile* profile) { |
| return static_cast<AdaptiveQuietNotificationPermissionUiEnabler*>( |
| GetInstance()->GetServiceForBrowserContext(profile, true)); |
| } |
| |
| // static |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory* |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory::GetInstance() { |
| return base::Singleton< |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory>::get(); |
| } |
| |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory::Factory() |
| : BrowserContextKeyedServiceFactory( |
| "AdaptiveQuietNotificationPermissionUiEnabler", |
| BrowserContextDependencyManager::GetInstance()) { |
| DependsOn(HostContentSettingsMapFactory::GetInstance()); |
| } |
| |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory::~Factory() {} |
| |
| KeyedService* |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory::BuildServiceInstanceFor( |
| content::BrowserContext* context) const { |
| return new AdaptiveQuietNotificationPermissionUiEnabler( |
| static_cast<Profile*>(context)); |
| } |
| |
| content::BrowserContext* |
| AdaptiveQuietNotificationPermissionUiEnabler::Factory::GetBrowserContextToUse( |
| content::BrowserContext* context) const { |
| return chrome::GetBrowserContextRedirectedInIncognito(context); |
| } |
| |
| // AdaptiveQuietNotificationPermissionUiEnabler ------------------------------ |
| |
| // static |
| AdaptiveQuietNotificationPermissionUiEnabler* |
| AdaptiveQuietNotificationPermissionUiEnabler::GetForProfile(Profile* profile) { |
| return AdaptiveQuietNotificationPermissionUiEnabler::Factory::GetForProfile( |
| profile); |
| } |
| |
| void AdaptiveQuietNotificationPermissionUiEnabler:: |
| RecordPermissionPromptOutcome(permissions::PermissionAction action) { |
| ListPrefUpdate update(profile_->GetPrefs(), |
| prefs::kNotificationPermissionActions); |
| // Discard permission actions older than |kPermissionActionMaxAge|. |
| const base::Time cutoff = clock_->Now() - kPermissionActionMaxAge; |
| update->EraseListValueIf([cutoff](const base::Value& entry) { |
| const base::Optional<base::Time> timestamp = |
| util::ValueToTime(entry.FindKey(kPermissionActionEntryTimestampKey)); |
| return !timestamp || *timestamp < cutoff; |
| }); |
| |
| // Record the new permission action. |
| base::DictionaryValue new_action_attributes; |
| new_action_attributes.SetKey(kPermissionActionEntryTimestampKey, |
| util::TimeToValue(clock_->Now())); |
| new_action_attributes.SetIntKey(kPermissionActionEntryActionKey, |
| static_cast<int>(action)); |
| update->Append(std::move(new_action_attributes)); |
| |
| if (!DidDenyLastThreeTimes(update->GetList(), clock_)) { |
| return; |
| } |
| |
| if (QuietNotificationPermissionUiConfig:: |
| IsAdaptiveActivationDryRunEnabled()) { |
| profile_->GetPrefs()->SetBoolean( |
| prefs::kHadThreeConsecutiveNotificationPermissionDenies, |
| true /* value */); |
| } |
| |
| if (QuietNotificationPermissionUiConfig::IsAdaptiveActivationEnabled() && |
| !profile_->GetPrefs()->GetBoolean( |
| prefs::kEnableQuietNotificationPermissionUi)) { |
| // Set |is_enabling_adaptively_| for the duration of the pref update to |
| // inform OnQuietUiStateChanged() that the quiet UI is being enabled |
| // adaptively, so that it can record the correct metrics. |
| base::AutoReset<bool> enabling_adaptively(&is_enabling_adaptively_, true); |
| profile_->GetPrefs()->SetBoolean( |
| prefs::kEnableQuietNotificationPermissionUi, true /* value */); |
| // TODO(crbug.com/1147467): If `kQuietNotificationPermissionShouldShowPromo` |
| // stops being a good indicator as to how the quiet UI pref was enabled, |
| // remove the |BackfillEnablingMethodIfMissing| logic. |
| profile_->GetPrefs()->SetBoolean( |
| prefs::kQuietNotificationPermissionShouldShowPromo, true /* value */); |
| } |
| } |
| |
| void AdaptiveQuietNotificationPermissionUiEnabler::ClearInteractionHistory( |
| const base::Time& delete_begin, |
| const base::Time& delete_end) { |
| DCHECK(!delete_end.is_null()); |
| |
| if (delete_begin.is_null() && delete_end.is_max()) { |
| profile_->GetPrefs()->ClearPref(prefs::kNotificationPermissionActions); |
| return; |
| } |
| |
| ListPrefUpdate update(profile_->GetPrefs(), |
| prefs::kNotificationPermissionActions); |
| |
| update->EraseListValueIf([delete_begin, delete_end](const auto& entry) { |
| const base::Optional<base::Time> timestamp = |
| util::ValueToTime(entry.FindKey(kPermissionActionEntryTimestampKey)); |
| return (!timestamp || |
| (*timestamp >= delete_begin && *timestamp < delete_end)); |
| }); |
| } |
| |
| AdaptiveQuietNotificationPermissionUiEnabler:: |
| AdaptiveQuietNotificationPermissionUiEnabler(Profile* profile) |
| : profile_(profile), clock_(base::DefaultClock::GetInstance()) { |
| pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>(); |
| pref_change_registrar_->Init(profile_->GetPrefs()); |
| pref_change_registrar_->Add( |
| prefs::kEnableQuietNotificationPermissionUi, |
| base::BindRepeating( |
| &AdaptiveQuietNotificationPermissionUiEnabler::OnQuietUiStateChanged, |
| base::Unretained(this))); |
| |
| // Record whether the quiet UI is enabled, but only when notifications are not |
| // completely blocked. |
| auto* host_content_settings_map = |
| HostContentSettingsMapFactory::GetForProfile(profile_); |
| const ContentSetting notifications_setting = |
| host_content_settings_map->GetDefaultContentSetting( |
| ContentSettingsType::NOTIFICATIONS, nullptr /* provider_id */); |
| if (notifications_setting != CONTENT_SETTING_BLOCK) { |
| const bool is_quiet_ui_enabled_in_prefs = profile_->GetPrefs()->GetBoolean( |
| prefs::kEnableQuietNotificationPermissionUi); |
| base::UmaHistogramBoolean(kIsQuietUiEnabledInPrefs, |
| is_quiet_ui_enabled_in_prefs); |
| } |
| |
| BackfillEnablingMethodIfMissing(); |
| } |
| |
| AdaptiveQuietNotificationPermissionUiEnabler:: |
| ~AdaptiveQuietNotificationPermissionUiEnabler() = default; |
| |
| void AdaptiveQuietNotificationPermissionUiEnabler::OnQuietUiStateChanged() { |
| const bool is_quiet_ui_enabled_in_prefs = profile_->GetPrefs()->GetBoolean( |
| prefs::kEnableQuietNotificationPermissionUi); |
| base::UmaHistogramBoolean(kQuietUiEnabledStateInPrefsChangedTo, |
| is_quiet_ui_enabled_in_prefs); |
| |
| if (is_quiet_ui_enabled_in_prefs) { |
| base::UmaHistogramBoolean(kDidAdaptivelyEnableQuietUiInPrefs, |
| is_enabling_adaptively_); |
| profile_->GetPrefs()->SetInteger( |
| prefs::kQuietNotificationPermissionUiEnablingMethod, |
| static_cast<int>(is_enabling_adaptively_ ? EnablingMethod::kAdaptive |
| : EnablingMethod::kManual)); |
| } else { |
| // Reset the promo state so that if the quiet UI is enabled adaptively |
| // again, the promo will be shown again. |
| profile_->GetPrefs()->ClearPref( |
| prefs::kQuietNotificationPermissionShouldShowPromo); |
| profile_->GetPrefs()->ClearPref( |
| prefs::kQuietNotificationPermissionPromoWasShown); |
| profile_->GetPrefs()->ClearPref( |
| prefs::kQuietNotificationPermissionUiEnablingMethod); |
| |
| // If the users has just turned off the quiet UI, clear interaction history |
| // so that if we are in adaptive mode, and the triggering conditions are |
| // met, we won't turn it back on immediately. |
| ClearInteractionHistory(base::Time(), base::Time::Max()); |
| } |
| } |
| |
| void AdaptiveQuietNotificationPermissionUiEnabler:: |
| BackfillEnablingMethodIfMissing() { |
| if (QuietNotificationPermissionUiState::GetQuietUiEnablingMethod(profile_) != |
| EnablingMethod::kUnspecified) { |
| return; |
| } |
| |
| // `kQuietNotificationPermissionUiEnablingMethod` was not populated prior to |
| // M88, but `kQuietNotificationPermissionShouldShowPromo` is a solid indicator |
| // as to how the setting was enabled in the first place because it's only set |
| // to true when the quiet UI has been enabled adaptively. |
| const bool has_enabled_adaptively = profile_->GetPrefs()->GetBoolean( |
| prefs::kQuietNotificationPermissionShouldShowPromo); |
| |
| profile_->GetPrefs()->SetInteger( |
| prefs::kQuietNotificationPermissionUiEnablingMethod, |
| static_cast<int>(has_enabled_adaptively ? EnablingMethod::kAdaptive |
| : EnablingMethod::kManual)); |
| } |