| // Copyright 2019 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/web_applications/web_app_prefs_utils.h" |
| |
| #include <memory> |
| |
| #include "base/json/values_util.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "chrome/browser/web_applications/web_app_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/webapps/browser/installable/installable_metrics.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| const char kLatestWebAppInstallSource[] = "latest_web_app_install_source"; |
| |
| const base::Value::Dict* GetWebAppDictionary(const PrefService* pref_service, |
| const AppId& app_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const base::Value::Dict& web_apps_prefs = |
| pref_service->GetDict(prefs::kWebAppsPreferences); |
| |
| return web_apps_prefs.FindDict(app_id); |
| } |
| |
| base::Value::Dict& UpdateWebAppDictionary( |
| ScopedDictPrefUpdate& web_apps_prefs_update, |
| const AppId& app_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return *web_apps_prefs_update->EnsureDict(app_id); |
| } |
| |
| // Returns whether the time occurred within X days. |
| bool TimeOccurredWithinDays(absl::optional<base::Time> time, int days) { |
| return time && (base::Time::Now() - time.value()).InDays() < days; |
| } |
| |
| // Removes all the empty app ID dictionaries from the `web_app_ids` dictionary. |
| // That is, this dictionary: |
| // |
| // "web_app_ids": { |
| // "<app_id_1>": {}, |
| // "<app_id_2>": { "foo": true } |
| // } |
| // |
| // will become this dictionary: |
| // |
| // "web_app_ids": { |
| // "<app_id_2>": { "foo": true } |
| // } |
| void RemoveEmptyWebAppPrefs(PrefService* pref_service) { |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsPreferences); |
| |
| std::vector<AppId> apps_to_remove; |
| for (const auto [app_id, dict] : *update) { |
| if (dict.is_dict() && dict.GetDict().empty()) |
| apps_to_remove.push_back(app_id); |
| } |
| |
| for (const AppId& app_id : apps_to_remove) |
| update->Remove(app_id); |
| } |
| |
| } // namespace |
| |
| // The stored preferences look like: |
| // "web_app_ids": { |
| // "<app_id_1>": { |
| // "was_external_app_uninstalled_by_user": true, |
| // "IPH_num_of_consecutive_ignore": 2, |
| // A string-flavored base::value representing the int64_t number of |
| // microseconds since the Windows epoch, using base::TimeToValue(). |
| // "IPH_last_ignore_time": "13249617864945580", |
| // A string-flavored base::value representing the int64_t number of |
| // microseconds since the Windows epoch, using base::TimeToValue(). |
| // "ML_last_time_install_ignored": "13249617864945580", |
| // A string-flavored base::value representing the int64_t number of |
| // microseconds since the Windows epoch, using base::TimeToValue(). |
| // "ML_last_time_install_dismissed": "13249617864945580", |
| // "ML_num_of_consecutive_not_accepted": 2, |
| // }, |
| // }, |
| // "app_agnostic_ml_state": { |
| // A string-flavored base::value representing the int64_t number of |
| // microseconds since the Windows epoch, using base::TimeToValue(). |
| // "ML_last_time_install_ignored": "13249617864945580", |
| // A string-flavored base::value representing the int64_t number of |
| // microseconds since the Windows epoch, using base::TimeToValue(). |
| // "ML_last_time_install_dismissed": "13249617864945580", |
| // "ML_num_of_consecutive_not_accepted": 2, |
| // }, |
| // "app_agnostic_iph_state": { |
| // "IPH_num_of_consecutive_ignore": 3, |
| // A string-flavored base::Value representing int64_t number of microseconds |
| // since the Windows epoch, using base::TimeToValue(). |
| // "IPH_last_ignore_time": "13249617864945500", |
| // }, |
| // isolation_state is managed by isolation_prefs_utils |
| // "isolation_state": { |
| // "<origin>": { |
| // "storage_isolation_key": "abc123", |
| // }, |
| // } |
| // |
| |
| const char kIphIgnoreCount[] = "IPH_num_of_consecutive_ignore"; |
| |
| const char kIphLastIgnoreTime[] = "IPH_last_ignore_time"; |
| |
| void WebAppPrefsUtilsRegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterDictionaryPref(::prefs::kWebAppsPreferences); |
| registry->RegisterDictionaryPref(::prefs::kWebAppsAppAgnosticIphState); |
| registry->RegisterDictionaryPref(::prefs::kWebAppsAppAgnosticMlState); |
| } |
| |
| bool GetBoolWebAppPref(const PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path) { |
| if (const base::Value::Dict* web_app_prefs = |
| GetWebAppDictionary(pref_service, app_id)) { |
| return web_app_prefs->FindBoolByDottedPath(path).value_or(false); |
| } |
| return false; |
| } |
| |
| void UpdateBoolWebAppPref(PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path, |
| bool value) { |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsPreferences); |
| |
| base::Value::Dict& web_app_prefs = UpdateWebAppDictionary(update, app_id); |
| web_app_prefs.SetByDottedPath(path, value); |
| } |
| |
| absl::optional<int> GetIntWebAppPref(const PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path) { |
| const base::Value::Dict* web_app_prefs = |
| GetWebAppDictionary(pref_service, app_id); |
| if (web_app_prefs) |
| return web_app_prefs->FindIntByDottedPath(path); |
| return absl::nullopt; |
| } |
| |
| void UpdateIntWebAppPref(PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path, |
| int value) { |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsPreferences); |
| |
| base::Value::Dict& web_app_prefs = UpdateWebAppDictionary(update, app_id); |
| web_app_prefs.SetByDottedPath(path, value); |
| } |
| |
| absl::optional<double> GetDoubleWebAppPref(const PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path) { |
| const base::Value::Dict* web_app_prefs = |
| GetWebAppDictionary(pref_service, app_id); |
| if (web_app_prefs) |
| return web_app_prefs->FindDoubleByDottedPath(path); |
| return absl::nullopt; |
| } |
| |
| void UpdateDoubleWebAppPref(PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path, |
| double value) { |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsPreferences); |
| |
| base::Value::Dict& web_app_prefs = UpdateWebAppDictionary(update, app_id); |
| web_app_prefs.SetByDottedPath(path, value); |
| } |
| |
| absl::optional<base::Time> GetTimeWebAppPref(const PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path) { |
| if (const auto* web_app_prefs = GetWebAppDictionary(pref_service, app_id)) { |
| if (auto* value = web_app_prefs->FindByDottedPath(path)) |
| return base::ValueToTime(value); |
| } |
| |
| return absl::nullopt; |
| } |
| |
| void UpdateTimeWebAppPref(PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path, |
| base::Time value) { |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsPreferences); |
| |
| auto& web_app_prefs = UpdateWebAppDictionary(update, app_id); |
| web_app_prefs.SetByDottedPath(path, base::TimeToValue(value)); |
| } |
| |
| void RemoveWebAppPref(PrefService* pref_service, |
| const AppId& app_id, |
| base::StringPiece path) { |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsPreferences); |
| |
| base::Value::Dict& web_app_prefs = UpdateWebAppDictionary(update, app_id); |
| web_app_prefs.RemoveByDottedPath(path); |
| } |
| |
| absl::optional<int> GetWebAppInstallSourceDeprecated(PrefService* prefs, |
| const AppId& app_id) { |
| absl::optional<int> value = |
| GetIntWebAppPref(prefs, app_id, kLatestWebAppInstallSource); |
| return value; |
| } |
| |
| std::map<AppId, int> TakeAllWebAppInstallSources(PrefService* pref_service) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| const base::Value* web_apps_prefs = |
| pref_service->GetUserPrefValue(prefs::kWebAppsPreferences); |
| if (!web_apps_prefs || !web_apps_prefs->is_dict()) |
| return {}; |
| |
| std::map<AppId, int> return_value; |
| for (auto item : web_apps_prefs->GetDict()) { |
| const AppId& app_id = item.first; |
| absl::optional<int> install_source = |
| item.second.GetDict().FindInt(kLatestWebAppInstallSource); |
| if (install_source) |
| return_value.insert(std::make_pair(app_id, *install_source)); |
| } |
| |
| for (const auto& item : return_value) |
| RemoveWebAppPref(pref_service, item.first, kLatestWebAppInstallSource); |
| |
| RemoveEmptyWebAppPrefs(pref_service); |
| |
| return return_value; |
| } |
| |
| void RecordInstallIphIgnored(PrefService* pref_service, |
| const AppId& app_id, |
| base::Time time) { |
| absl::optional<int> ignored_count = |
| GetIntWebAppPref(pref_service, app_id, kIphIgnoreCount); |
| int new_count = base::saturated_cast<int>(1 + ignored_count.value_or(0)); |
| |
| UpdateIntWebAppPref(pref_service, app_id, kIphIgnoreCount, new_count); |
| UpdateTimeWebAppPref(pref_service, app_id, kIphLastIgnoreTime, time); |
| |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsAppAgnosticIphState); |
| int global_count = update->FindInt(kIphIgnoreCount).value_or(0); |
| update->Set(kIphIgnoreCount, base::saturated_cast<int>(global_count + 1)); |
| update->Set(kIphLastIgnoreTime, base::TimeToValue(time)); |
| } |
| |
| void RecordInstallIphInstalled(PrefService* pref_service, const AppId& app_id) { |
| // The ignored count is meant to track consecutive occurrences of the user |
| // ignoring IPH, to help determine when IPH should be muted. Therefore |
| // resetting ignored count on successful install. |
| UpdateIntWebAppPref(pref_service, app_id, kIphIgnoreCount, 0); |
| |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsAppAgnosticIphState); |
| update->Set(kIphIgnoreCount, 0); |
| } |
| |
| bool ShouldShowIph(PrefService* pref_service, const AppId& app_id) { |
| // Do not show IPH if the user ignored the last N+ promos for this app. |
| int app_ignored_count = |
| GetIntWebAppPref(pref_service, app_id, kIphIgnoreCount).value_or(0); |
| if (app_ignored_count >= kIphMuteAfterConsecutiveAppSpecificIgnores) |
| return false; |
| // Do not show IPH if the user ignored a promo for this app within N days. |
| auto app_last_ignore = |
| GetTimeWebAppPref(pref_service, app_id, kIphLastIgnoreTime); |
| if (TimeOccurredWithinDays(app_last_ignore, |
| kIphAppSpecificMuteTimeSpanDays)) { |
| return false; |
| } |
| |
| const base::Value::Dict& dict = |
| pref_service->GetDict(prefs::kWebAppsAppAgnosticIphState); |
| |
| // Do not show IPH if the user ignored the last N+ promos for any app. |
| int global_ignored_count = dict.FindInt(kIphIgnoreCount).value_or(0); |
| if (global_ignored_count >= kIphMuteAfterConsecutiveAppAgnosticIgnores) |
| return false; |
| // Do not show IPH if the user ignored a promo for any app within N days. |
| auto global_last_ignore = base::ValueToTime(dict.Find(kIphLastIgnoreTime)); |
| if (TimeOccurredWithinDays(global_last_ignore, |
| kIphAppAgnosticMuteTimeSpanDays)) { |
| return false; |
| } |
| return true; |
| } |
| |
| const char kLastTimeMlInstallIgnored[] = "ML_last_time_install_ignored"; |
| const char kLastTimeMlInstallDismissed[] = "ML_last_time_install_dismissed"; |
| const char kConsecutiveMlInstallNotAcceptedCount[] = |
| "ML_num_of_consecutive_not_accepted"; |
| |
| void RecordMlInstallIgnored(PrefService* pref_service, |
| const AppId& app_id, |
| base::Time time) { |
| CHECK(pref_service); |
| |
| absl::optional<int> ignored_count = GetIntWebAppPref( |
| pref_service, app_id, kConsecutiveMlInstallNotAcceptedCount); |
| int new_count = base::saturated_cast<int>(1 + ignored_count.value_or(0)); |
| |
| UpdateIntWebAppPref(pref_service, app_id, |
| kConsecutiveMlInstallNotAcceptedCount, new_count); |
| UpdateTimeWebAppPref(pref_service, app_id, kLastTimeMlInstallIgnored, time); |
| |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsAppAgnosticMlState); |
| int global_count = |
| update->FindInt(kConsecutiveMlInstallNotAcceptedCount).value_or(0); |
| update->Set(kConsecutiveMlInstallNotAcceptedCount, |
| base::saturated_cast<int>(global_count + 1)); |
| update->Set(kLastTimeMlInstallIgnored, base::TimeToValue(time)); |
| } |
| void RecordMlInstallDismissed(PrefService* pref_service, |
| const AppId& app_id, |
| base::Time time) { |
| CHECK(pref_service); |
| |
| absl::optional<int> ignored_count = |
| GetIntWebAppPref(pref_service, app_id, kLastTimeMlInstallDismissed); |
| int new_count = base::saturated_cast<int>(1 + ignored_count.value_or(0)); |
| |
| UpdateIntWebAppPref(pref_service, app_id, kLastTimeMlInstallDismissed, |
| new_count); |
| UpdateTimeWebAppPref(pref_service, app_id, kLastTimeMlInstallIgnored, time); |
| |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsAppAgnosticMlState); |
| int global_count = update->FindInt(kLastTimeMlInstallDismissed).value_or(0); |
| update->Set(kConsecutiveMlInstallNotAcceptedCount, |
| base::saturated_cast<int>(global_count + 1)); |
| update->Set(kLastTimeMlInstallDismissed, base::TimeToValue(time)); |
| } |
| |
| void RecordMlInstallAccepted(PrefService* pref_service, |
| const AppId& app_id, |
| base::Time time) { |
| // The ignored count is meant to track consecutive occurrences of the user |
| // ignoring ML install, to help determine when ML install should be muted. |
| // Therefore resetting ignored count on successful install. |
| UpdateIntWebAppPref(pref_service, app_id, |
| kConsecutiveMlInstallNotAcceptedCount, 0); |
| |
| ScopedDictPrefUpdate update(pref_service, prefs::kWebAppsAppAgnosticMlState); |
| update->Set(kConsecutiveMlInstallNotAcceptedCount, 0); |
| } |
| |
| bool IsMlPromotionBlockedByHistoryGuardrail(PrefService* pref_service, |
| const AppId& app_id) { |
| constexpr int kMuteMlInstallAfterConsecutiveAppSpecificIgnores = 3; |
| constexpr int kMuteMlInstallAfterIgnoreForDays = 2; |
| constexpr int kMuteMlInstallAfterDismissForDays = 14; |
| |
| constexpr int kMuteMlInstallAfterConsecutiveAppAgnosticIgnores = 5; |
| constexpr int kMuteMlInstallAfterAnyIgnoreForDays = 1; |
| constexpr int kMuteMlInstallAfterAnyDismissForDays = 7; |
| |
| // Do not show Ml install if the user ignored the last N+ promos for this app. |
| int app_ignored_count = |
| GetIntWebAppPref(pref_service, app_id, |
| kConsecutiveMlInstallNotAcceptedCount) |
| .value_or(0); |
| if (app_ignored_count >= kMuteMlInstallAfterConsecutiveAppSpecificIgnores) { |
| return true; |
| } |
| // Do not show Ml install if the user ignored a promo for this app within N |
| // days. |
| auto app_last_ignore = |
| GetTimeWebAppPref(pref_service, app_id, kLastTimeMlInstallIgnored); |
| if (TimeOccurredWithinDays(app_last_ignore, |
| kMuteMlInstallAfterIgnoreForDays)) { |
| return true; |
| } |
| // Do not show Ml install if the user dismissed a promo for this app within N |
| // days. |
| auto app_last_dismissed = |
| GetTimeWebAppPref(pref_service, app_id, kLastTimeMlInstallDismissed); |
| if (TimeOccurredWithinDays(app_last_dismissed, |
| kMuteMlInstallAfterDismissForDays)) { |
| return true; |
| } |
| |
| const base::Value::Dict& dict = |
| pref_service->GetDict(prefs::kWebAppsAppAgnosticMlState); |
| |
| // Do not show Ml install if the user ignored the last N+ promos for any app. |
| int global_ignored_count = |
| dict.FindInt(kConsecutiveMlInstallNotAcceptedCount).value_or(0); |
| if (global_ignored_count >= |
| kMuteMlInstallAfterConsecutiveAppAgnosticIgnores) { |
| return true; |
| } |
| |
| // Do not show Ml install if the user ignored a promo for any app within N |
| // days. |
| auto global_last_ignore = |
| base::ValueToTime(dict.Find(kConsecutiveMlInstallNotAcceptedCount)); |
| if (TimeOccurredWithinDays(global_last_ignore, |
| kMuteMlInstallAfterAnyIgnoreForDays)) { |
| return true; |
| } |
| // Do not show Ml install if the user ignored a promo for any app within N |
| // days. |
| auto global_last_dismiss = |
| base::ValueToTime(dict.Find(kLastTimeMlInstallDismissed)); |
| if (TimeOccurredWithinDays(global_last_dismiss, |
| kMuteMlInstallAfterAnyDismissForDays)) { |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace web_app |