// Copyright 2020 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 "ash/controls/contextual_tooltip.h" #include "ash/constants/ash_features.h" #include "ash/constants/ash_pref_names.h" #include "ash/constants/ash_switches.h" #include "ash/public/cpp/shelf_config.h" #include "ash/session/session_controller_impl.h" #include "ash/shell.h" #include "base/json/values_util.h" #include "base/no_destructor.h" #include "base/strings/strcat.h" #include "base/strings/string_util.h" #include "base/time/time.h" #include "components/prefs/scoped_user_pref_update.h" namespace ash { namespace contextual_tooltip { namespace { // Keys for tooltip sub-preferences for shown count and last time shown. constexpr char kShownCount[] = "shown_count"; constexpr char kLastTimeShown[] = "last_time_shown"; // Keys for tooltip sub-preferences of how many times a gesture has been // successfully performed by the user. constexpr char kSuccessCount[] = "success_count"; // Whether the drag handle nudge cannot be shown because the shelf is currently // hidden - used to unblock showing back gesture when shelf is hidden (the back // gesture will normally show only if the drag handle nudge has already been // shown within the last nudge show interval). bool g_drag_handle_nudge_disabled_for_hidden_shelf = false; // Whether the back gesture nudge is currently being shown. bool g_back_gesture_nudge_showing = false; base::Clock* g_clock_override = nullptr; base::Time GetTime() { if (g_clock_override) return g_clock_override->Now(); return base::Time::Now(); } std::string TooltipTypeToString(TooltipType type) { switch (type) { case TooltipType::kBackGesture: return "back_gesture"; case TooltipType::kHomeToOverview: return "home_to_overview"; case TooltipType::kInAppToHome: return "in_app_to_home"; case TooltipType::kKeyboardBacklightColor: return "keyboard_backlight_color"; case TooltipType::kKeyboardBacklightWallpaperColor: return "keyboard_backlight_wallpaper_color"; } return "invalid"; } // Creates the path to the dictionary value from the contextual tooltip type and // the sub-preference. std::string GetPath(TooltipType type, const std::string& sub_pref) { return base::JoinString({TooltipTypeToString(type), sub_pref}, "."); } base::Time GetLastShownTime(PrefService* prefs, TooltipType type) { const base::Value* last_shown_time = prefs->GetValueDict(prefs::kContextualTooltips) .FindByDottedPath(GetPath(type, kLastTimeShown)); if (!last_shown_time) return base::Time(); return *base::ValueToTime(last_shown_time); } int GetSuccessCount(PrefService* prefs, TooltipType type) { absl::optional success_count = prefs->GetValueDict(prefs::kContextualTooltips) .FindIntByDottedPath(GetPath(type, kSuccessCount)); return success_count.value_or(0); } const absl::optional& GetMinIntervalOverride() { // Overridden minimum time between showing contextual nudges to the user. static absl::optional min_interval_override; if (!min_interval_override) { min_interval_override = switches::ContextualNudgesInterval(); } return min_interval_override; } } // namespace void RegisterProfilePrefs(PrefRegistrySimple* registry) { if (features::AreContextualNudgesEnabled()) registry->RegisterDictionaryPref(prefs::kContextualTooltips); } bool ShouldShowNudge(PrefService* prefs, TooltipType type, base::TimeDelta* recheck_delay) { auto set_recheck_delay = [&recheck_delay](base::TimeDelta delay) { if (recheck_delay) *recheck_delay = delay; }; if (!features::AreContextualNudgesEnabled()) { set_recheck_delay(base::TimeDelta()); return false; } if (type == TooltipType::kInAppToHome && g_drag_handle_nudge_disabled_for_hidden_shelf) { set_recheck_delay(base::TimeDelta()); return false; } const int success_count = GetSuccessCount(prefs, type); if ((type == TooltipType::kHomeToOverview && success_count >= kSuccessLimitHomeToOverview) || (type == TooltipType::kBackGesture && success_count >= kSuccessLimitBackGesture) || (type == TooltipType::kInAppToHome && success_count >= kSuccessLimitInAppToHome) || (type == TooltipType::kKeyboardBacklightColor && success_count >= kSuccessLimitKeyboardBacklightColor)) { set_recheck_delay(base::TimeDelta()); return false; } const int shown_count = GetShownCount(prefs, type); if (shown_count >= kNotificationLimit) { set_recheck_delay(base::TimeDelta()); return false; } // Before showing back gesture nudge, do not show it if in-app to shelf nudge // should be shown (to prevent two nudges from showing up at the same time). // Verify that the in-app to home nudge was shown within the last show // interval. if (type == TooltipType::kBackGesture) { if (!g_drag_handle_nudge_disabled_for_hidden_shelf && ShouldShowNudge(prefs, TooltipType::kInAppToHome, nullptr)) { set_recheck_delay(kMinIntervalBetweenBackAndDragHandleNudge); return false; } // Verify that drag handle nudge has been shown at least a minute ago. const base::Time drag_handle_nudge_last_shown_time = GetLastShownTime(prefs, TooltipType::kInAppToHome); if (!drag_handle_nudge_last_shown_time.is_null()) { const base::TimeDelta time_since_drag_handle_nudge = GetTime() - drag_handle_nudge_last_shown_time; if (time_since_drag_handle_nudge < kMinIntervalBetweenBackAndDragHandleNudge) { set_recheck_delay(kMinIntervalBetweenBackAndDragHandleNudge - time_since_drag_handle_nudge); return false; } } } // Make sure that drag handle nudge is not shown within a minute of back // gesture nudge. if (type == TooltipType::kInAppToHome) { if (g_back_gesture_nudge_showing) { set_recheck_delay(kMinIntervalBetweenBackAndDragHandleNudge); return false; } const base::Time back_nudge_last_shown_time = GetLastShownTime(prefs, TooltipType::kBackGesture); if (!back_nudge_last_shown_time.is_null()) { const base::TimeDelta time_since_back_nudge = GetTime() - back_nudge_last_shown_time; if (time_since_back_nudge < kMinIntervalBetweenBackAndDragHandleNudge) { set_recheck_delay(kMinIntervalBetweenBackAndDragHandleNudge - time_since_back_nudge); return false; } } } if (shown_count == 0) return true; const base::Time last_shown_time = GetLastShownTime(prefs, type); const base::TimeDelta min_interval = GetMinIntervalOverride().value_or(kMinInterval); const base::TimeDelta time_since_last_nudge = GetTime() - last_shown_time; if (time_since_last_nudge < min_interval) { set_recheck_delay(min_interval - time_since_last_nudge); return false; } return true; } base::TimeDelta GetNudgeTimeout(PrefService* prefs, TooltipType type) { const int shown_count = GetShownCount(prefs, type); if (shown_count == 0) return base::TimeDelta(); return kNudgeShowDuration; } int GetShownCount(PrefService* prefs, TooltipType type) { absl::optional shown_count = prefs->GetValueDict(prefs::kContextualTooltips) .FindIntByDottedPath(GetPath(type, kShownCount)); return shown_count.value_or(0); } void HandleNudgeShown(PrefService* prefs, TooltipType type) { const int shown_count = GetShownCount(prefs, type); DictionaryPrefUpdate update(prefs, prefs::kContextualTooltips); update->SetIntPath(GetPath(type, kShownCount), shown_count + 1); update->SetPath(GetPath(type, kLastTimeShown), base::TimeToValue(GetTime())); } void HandleGesturePerformed(PrefService* prefs, TooltipType type) { const int success_count = GetSuccessCount(prefs, type); DictionaryPrefUpdate update(prefs, prefs::kContextualTooltips); update->SetIntPath(GetPath(type, kSuccessCount), success_count + 1); } void SetDragHandleNudgeDisabledForHiddenShelf(bool nudge_disabled) { g_drag_handle_nudge_disabled_for_hidden_shelf = nudge_disabled; } void SetBackGestureNudgeShowing(bool showing) { g_back_gesture_nudge_showing = showing; } void ClearPrefs() { DCHECK(Shell::Get()->session_controller()->GetLastActiveUserPrefService()); DictionaryPrefUpdate update( Shell::Get()->session_controller()->GetLastActiveUserPrefService(), prefs::kContextualTooltips); base::Value* nudges_dict = update.Get(); if (nudges_dict && !nudges_dict->DictEmpty()) nudges_dict->DictClear(); } void OverrideClockForTesting(base::Clock* test_clock) { DCHECK(!g_clock_override); g_clock_override = test_clock; } void ClearClockOverrideForTesting() { DCHECK(g_clock_override); g_clock_override = nullptr; } } // namespace contextual_tooltip } // namespace ash