| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/safety_hub/menu_notification_service.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/ui/safety_hub/menu_notification.h" |
| #include "chrome/browser/ui/safety_hub/notification_permission_review_service.h" |
| #include "chrome/browser/ui/safety_hub/revoked_permissions_service.h" |
| #include "chrome/browser/ui/safety_hub/safe_browsing_result.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_constants.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_prefs.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_result.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_service.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/core/common/safe_browsing_prefs.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/ui/safety_hub/password_status_check_result_android.h" |
| #else // BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/ui/safety_hub/extensions_result.h" |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| namespace { |
| |
| // Interval to show notification for compromised password in Safety Hub |
| // notifications. |
| const base::TimeDelta kPasswordCheckNotificationInterval = base::Days(0); |
| |
| // Interval to show notification for revoked permissions in Safety Hub |
| // notifications. |
| const base::TimeDelta kRevokedPermissionsNotificationInterval = base::Days(10); |
| |
| // Interval to show notification for notification permissions in Safety Hub |
| // notifications. |
| const base::TimeDelta kNotificationPermissionsNotificationInterval = |
| base::Days(10); |
| |
| // Interval to show notification for safe browsing in Safety Hub notifications. |
| const base::TimeDelta kSafeBrowsingNotificationInterval = base::Days(90); |
| |
| SafetyHubModuleInfoElement::SafetyHubModuleInfoElement() = default; |
| SafetyHubModuleInfoElement::~SafetyHubModuleInfoElement() = default; |
| |
| SafetyHubModuleInfoElement::SafetyHubModuleInfoElement( |
| MenuNotificationPriority priority, |
| base::TimeDelta interval, |
| base::RepeatingCallback<std::optional<std::unique_ptr<SafetyHubResult>>()> |
| result_getter, |
| std::unique_ptr<SafetyHubMenuNotification> notification) |
| : priority(priority), |
| interval(interval), |
| result_getter(result_getter), |
| notification(std::move(notification)) {} |
| } // namespace |
| |
| SafetyHubMenuNotificationService::SafetyHubMenuNotificationService( |
| PrefService* pref_service, |
| RevokedPermissionsService* revoked_permissions_service, |
| NotificationPermissionsReviewService* notification_permissions_service, |
| #if !BUILDFLAG(IS_ANDROID) |
| PasswordStatusCheckService* password_check_service, |
| #endif // !BUILDFLAG(IS_ANDROID) |
| Profile* profile) { |
| pref_service_ = std::move(pref_service); |
| const base::Value::Dict& stored_notifications = |
| pref_service_->GetDict(safety_hub_prefs::kMenuNotificationsPrefsKey); |
| |
| pref_dict_key_map_ = { |
| {safety_hub::SafetyHubModuleType::UNUSED_SITE_PERMISSIONS, |
| "unused-site-permissions"}, |
| {safety_hub::SafetyHubModuleType::SAFE_BROWSING, "safe-browsing"}, |
| }; |
| |
| // The Safety Hub services will be available whenever the |GetCachedResult| |
| // method is called, so it is safe to use |base::Unretained| here. |
| SetInfoElement( |
| safety_hub::SafetyHubModuleType::UNUSED_SITE_PERMISSIONS, |
| MenuNotificationPriority::LOW, kRevokedPermissionsNotificationInterval, |
| base::BindRepeating(&SafetyHubService::GetCachedResult, |
| base::Unretained(revoked_permissions_service)), |
| stored_notifications); |
| if (!base::FeatureList::IsEnabled( |
| features::kSafetyHubDisruptiveNotificationRevocation) || |
| features::kSafetyHubDisruptiveNotificationRevocationShadowRun.Get()) { |
| pref_dict_key_map_ |
| [safety_hub::SafetyHubModuleType::NOTIFICATION_PERMISSIONS] = |
| "notification-permissions"; |
| SetInfoElement( |
| safety_hub::SafetyHubModuleType::NOTIFICATION_PERMISSIONS, |
| MenuNotificationPriority::LOW, |
| kNotificationPermissionsNotificationInterval, |
| base::BindRepeating(&SafetyHubService::GetCachedResult, |
| base::Unretained(notification_permissions_service)), |
| stored_notifications); |
| } |
| SetInfoElement(safety_hub::SafetyHubModuleType::SAFE_BROWSING, |
| MenuNotificationPriority::MEDIUM, |
| kSafeBrowsingNotificationInterval, |
| base::BindRepeating(&SafetyHubSafeBrowsingResult::GetResult, |
| base::Unretained(pref_service)), |
| stored_notifications); |
| |
| // Extensions are not available on Android, so we cannot fetch any information |
| // about them. Passwords are handled by GMS Core on Android and our |
| // PasswordStatusCheckService is not compatible with GMS Core. |
| #if !BUILDFLAG(IS_ANDROID) |
| pref_dict_key_map_.emplace(safety_hub::SafetyHubModuleType::EXTENSIONS, |
| "extensions"); |
| SetInfoElement( |
| safety_hub::SafetyHubModuleType::EXTENSIONS, |
| MenuNotificationPriority::LOW, base::Days(10), |
| base::BindRepeating(&SafetyHubExtensionsResult::GetResult, profile, true), |
| stored_notifications); |
| |
| // PasswordStatusCheckService might be null for some profiles and testing. Add |
| // the info item only if the service is available. |
| if (password_check_service) { |
| pref_dict_key_map_.emplace(safety_hub::SafetyHubModuleType::PASSWORDS, |
| "passwords"); |
| SetInfoElement( |
| safety_hub::SafetyHubModuleType::PASSWORDS, |
| MenuNotificationPriority::HIGH, kPasswordCheckNotificationInterval, |
| base::BindRepeating(&PasswordStatusCheckService::GetCachedResult, |
| base::Unretained(password_check_service)), |
| stored_notifications); |
| } |
| #else // !BUILDFLAG(IS_ANDROID) |
| pref_dict_key_map_.emplace(safety_hub::SafetyHubModuleType::PASSWORDS, |
| "passwords"); |
| SetInfoElement( |
| safety_hub::SafetyHubModuleType::PASSWORDS, |
| MenuNotificationPriority::HIGH, kPasswordCheckNotificationInterval, |
| base::BindRepeating(&PasswordStatusCheckResultAndroid::GetResult, |
| base::Unretained(pref_service)), |
| stored_notifications); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| // Listen for changes to the Safe Browsing pref to accommodate the trigger |
| // logic. |
| registrar_.Init(pref_service); |
| registrar_.Add( |
| prefs::kSafeBrowsingEnabled, |
| base::BindRepeating( |
| &SafetyHubMenuNotificationService::OnSafeBrowsingPrefUpdate, |
| base::Unretained(this))); |
| } |
| |
| void SafetyHubMenuNotificationService::UpdateResultGetterForTesting( |
| safety_hub::SafetyHubModuleType type, |
| base::RepeatingCallback<std::optional<std::unique_ptr<SafetyHubResult>>()> |
| result_getter) { |
| module_info_map_[type]->result_getter = result_getter; |
| } |
| |
| SafetyHubMenuNotificationService::~SafetyHubMenuNotificationService() { |
| registrar_.RemoveAll(); |
| } |
| |
| std::optional<MenuNotificationEntry> |
| SafetyHubMenuNotificationService::GetNotificationToShow() { |
| std::optional<ResultMap> result_map = GetResultsFromAllModules(); |
| if (!result_map.has_value()) { |
| return std::nullopt; |
| } |
| std::list<SafetyHubMenuNotification*> notifications_to_be_shown; |
| MenuNotificationPriority cur_highest_priority = MenuNotificationPriority::LOW; |
| for (auto& item : result_map.value()) { |
| const SafetyHubModuleInfoElement* info_element = |
| module_info_map_[item.first].get(); |
| SafetyHubMenuNotification* notification = info_element->notification.get(); |
| // The result in the ResultMap (item.second) is being moved away from and |
| // thus shouldn't be used again in this method. |
| notification->UpdateResult(std::move(item.second)); |
| int max_all_time_impressions = |
| item.first == safety_hub::SafetyHubModuleType::SAFE_BROWSING ? 3 : 0; |
| if (notification->ShouldBeShown(info_element->interval, |
| max_all_time_impressions)) { |
| // Notifications are first sorted by priority, and then by being |
| // currently active. |
| if (info_element->priority > cur_highest_priority || |
| (info_element->priority == cur_highest_priority && |
| notification->IsCurrentlyActive())) { |
| cur_highest_priority = info_element->priority; |
| notifications_to_be_shown.push_front(notification); |
| } else { |
| notifications_to_be_shown.push_back(notification); |
| } |
| } else { |
| if (notification->IsCurrentlyActive()) { |
| notification->Dismiss(); |
| } |
| } |
| } |
| if (notifications_to_be_shown.empty()) { |
| // The notifications should be persisted with updated results. |
| SaveNotificationsToPrefs(); |
| return std::nullopt; |
| } |
| SafetyHubMenuNotification* notification_to_show = |
| notifications_to_be_shown.front(); |
| // Dismiss all other notifications that are not shown. |
| for (auto it = std::next(notifications_to_be_shown.begin()); |
| it != notifications_to_be_shown.end(); ++it) { |
| (*it)->Dismiss(); |
| } |
| notification_to_show->Show(); |
| |
| // The information related to showing the notification needs to be persisted |
| // as well. |
| SaveNotificationsToPrefs(); |
| return MenuNotificationEntry(notification_to_show->GetNotificationCommandId(), |
| notification_to_show->GetNotificationString(), |
| notification_to_show->GetModuleType()); |
| } |
| |
| std::optional<ResultMap> |
| SafetyHubMenuNotificationService::GetResultsFromAllModules() { |
| ResultMap result_map; |
| for (auto const& item : module_info_map_) { |
| CHECK(item.second->result_getter); |
| std::optional<std::unique_ptr<SafetyHubResult>> result = |
| item.second->result_getter.Run(); |
| // If one of the cached results is unavailable, no notification is shown. |
| if (!result.has_value()) { |
| return std::nullopt; |
| } |
| result_map.try_emplace(item.first, std::move(result.value())); |
| } |
| return result_map; |
| } |
| |
| void SafetyHubMenuNotificationService::SaveNotificationsToPrefs() const { |
| base::Value::Dict notifications; |
| for (auto const& it : pref_dict_key_map_) { |
| SafetyHubModuleInfoElement* info_element = |
| module_info_map_.find(it.first)->second.get(); |
| notifications.Set(it.second, info_element->notification->ToDictValue()); |
| } |
| pref_service_->SetDict(safety_hub_prefs::kMenuNotificationsPrefsKey, |
| std::move(notifications)); |
| } |
| |
| std::unique_ptr<SafetyHubMenuNotification> |
| SafetyHubMenuNotificationService::GetNotificationFromDict( |
| const base::Value::Dict& dict, |
| safety_hub::SafetyHubModuleType& type) const { |
| // It can be assumed that all `safety_hub::SafetyHubModuleType`s are in |
| // `pref_dict_key_map_`. |
| const base::Value::Dict* notification_dict = |
| dict.FindDict(pref_dict_key_map_.find(type)->second); |
| if (!notification_dict) { |
| auto new_notification = std::make_unique<SafetyHubMenuNotification>(type); |
| if (type == safety_hub::SafetyHubModuleType::SAFE_BROWSING) { |
| new_notification->SetOnlyShowAfter(base::Time::Now() + base::Days(1)); |
| } |
| return new_notification; |
| } |
| return std::make_unique<SafetyHubMenuNotification>(*notification_dict, type); |
| } |
| |
| void SafetyHubMenuNotificationService::SetInfoElement( |
| safety_hub::SafetyHubModuleType type, |
| MenuNotificationPriority priority, |
| base::TimeDelta interval, |
| base::RepeatingCallback<std::optional<std::unique_ptr<SafetyHubResult>>()> |
| result_getter, |
| const base::Value::Dict& stored_notifications) { |
| module_info_map_[type] = std::make_unique<SafetyHubModuleInfoElement>( |
| priority, interval, result_getter, |
| GetNotificationFromDict(stored_notifications, type)); |
| } |
| |
| void SafetyHubMenuNotificationService::OnSafeBrowsingPrefUpdate() { |
| module_info_map_[safety_hub::SafetyHubModuleType::SAFE_BROWSING] |
| ->notification->SetOnlyShowAfter(base::Time::Now() + base::Days(1)); |
| module_info_map_[safety_hub::SafetyHubModuleType::SAFE_BROWSING] |
| ->notification->ResetAllTimeNotificationCount(); |
| SaveNotificationsToPrefs(); |
| } |
| |
| void SafetyHubMenuNotificationService::DismissActiveNotification() { |
| for (auto const& item : module_info_map_) { |
| if (item.second->notification->IsCurrentlyActive()) { |
| item.second->notification->Dismiss(); |
| } |
| } |
| } |
| |
| void SafetyHubMenuNotificationService::DismissActiveNotificationOfModule( |
| safety_hub::SafetyHubModuleType module) { |
| // Callers of this function do not know if the module is available. Do |
| // nothing, if the module is not available. |
| if (!module_info_map_.contains(module)) { |
| return; |
| } |
| SafetyHubMenuNotification* notification = |
| module_info_map_.at(module)->notification.get(); |
| if (notification->IsCurrentlyActive()) { |
| notification->Dismiss(); |
| } |
| } |
| |
| bool SafetyHubMenuNotificationService::HasAnyNotificationBeenShown() const { |
| for (auto const& it : pref_dict_key_map_) { |
| SafetyHubModuleInfoElement* info_element = |
| module_info_map_.find(it.first)->second.get(); |
| if (info_element->notification.get()->HasAnyNotificationBeenShown()) { |
| return true; |
| } |
| } |
| return false; |
| } |