chromium/ash/shelf/launcher_nudge_controller.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/shelf/launcher_nudge_controller.h"

#include <memory>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/session/session_types.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/home_button.h"
#include "ash/shelf/home_button_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_navigation_widget.h"
#include "ash/shell.h"
#include "base/command_line.h"
#include "base/json/values_util.h"
#include "base/time/time.h"
#include "base/timer/wall_clock_timer.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/tablet_state.h"

namespace ash {
namespace {

// Keys for user preferences.
constexpr char kShownCount[] = "shown_count";
constexpr char kLastShownTime[] = "last_shown_time";
constexpr char kFirstLoginTime[] = "first_login_time";
constexpr char kWasLauncherShown[] = "was_launcher_shown";

// Constants for launcher nudge controller.
constexpr base::TimeDelta kFirstTimeShowNudgeInterval = base::Days(1);
constexpr base::TimeDelta kShowNudgeInterval = base::Days(1);
constexpr base::TimeDelta kFirstTimeShowNudgeIntervalForTest = base::Minutes(3);
constexpr base::TimeDelta kShowNudgeIntervalForTest = base::Minutes(3);

// Returns the last active user pref service.
PrefService* GetPrefs() {
  return Shell::Get()->session_controller()->GetLastActiveUserPrefService();
}

// Gets the timestamp when the nudge was last shown.
base::Time GetLastShownTime(PrefService* prefs) {
  const base::Value::Dict& dictionary =
      prefs->GetDict(prefs::kShelfLauncherNudge);
  std::optional<base::Time> last_shown_time =
      base::ValueToTime(dictionary.Find(kLastShownTime));
  return last_shown_time.value_or(base::Time());
}

// Gets the timestamp when the user first logged in. The value will not be
// set if the user has logged in before the launcher nudge feature was
// enabled.
base::Time GetFirstLoginTime(PrefService* prefs) {
  const base::Value::Dict& dictionary =
      prefs->GetDict(prefs::kShelfLauncherNudge);
  std::optional<base::Time> first_login_time =
      base::ValueToTime(dictionary.Find(kFirstLoginTime));
  return first_login_time.value_or(base::Time());
}

// Returns true if the launcher has been shown before.
bool WasLauncherShownPreviously(PrefService* prefs) {
  const base::Value::Dict& dictionary =
      prefs->GetDict(prefs::kShelfLauncherNudge);
  return dictionary.FindBool(kWasLauncherShown).value_or(false);
}

}  // namespace

// static
constexpr base::TimeDelta
    LauncherNudgeController::kMinIntervalAfterHomeButtonAppears;

LauncherNudgeController::LauncherNudgeController()
    : show_nudge_timer_(std::make_unique<base::WallClockTimer>()) {
  Shell::Get()->app_list_controller()->AddObserver(this);
}

LauncherNudgeController::~LauncherNudgeController() {
  if (Shell::Get()->app_list_controller())
    Shell::Get()->app_list_controller()->RemoveObserver(this);
}

// static
void LauncherNudgeController::RegisterProfilePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(prefs::kShelfLauncherNudge);
}

// static
HomeButton* LauncherNudgeController::GetHomeButtonForDisplay(
    int64_t display_id) {
  return Shell::Get()
      ->GetRootWindowControllerWithDisplayId(display_id)
      ->shelf()
      ->navigation_widget()
      ->GetHomeButton();
}

// static
int LauncherNudgeController::GetShownCount(PrefService* prefs) {
  const base::Value::Dict& dictionary =
      prefs->GetDict(prefs::kShelfLauncherNudge);
  return dictionary.FindInt(kShownCount).value_or(0);
}

base::TimeDelta LauncherNudgeController::GetNudgeInterval(
    bool is_first_time) const {
  if (features::IsLauncherNudgeShortIntervalEnabled()) {
    return is_first_time ? kFirstTimeShowNudgeIntervalForTest
                         : kShowNudgeIntervalForTest;
  }
  return is_first_time ? kFirstTimeShowNudgeInterval : kShowNudgeInterval;
}

void LauncherNudgeController::SetClockForTesting(
    const base::Clock* clock,
    const base::TickClock* timer_clock) {
  DCHECK(!show_nudge_timer_->IsRunning());
  show_nudge_timer_ =
      std::make_unique<base::WallClockTimer>(clock, timer_clock);
  clock_for_test_ = clock;
}

bool LauncherNudgeController::IsRecheckTimerRunningForTesting() {
  return show_nudge_timer_->IsRunning();
}

bool LauncherNudgeController::ShouldShowNudge(base::Time& recheck_time) const {
  PrefService* prefs = GetPrefs();
  if (!prefs)
    return false;

  // Do not show if the command line flag to hide nudges is set.
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kAshNoNudges))
    return false;

  if (GetFirstLoginTime(prefs).is_null()) {
    // Don't show the nudge to existing users. See
    // `OnActiveUserPrefServiceChanged()` for details.
    return false;
  }

  // Only show the launcher nudge in clamshell mode.
  if (Shell::Get()->IsInTabletMode())
    return false;

  // If the shown count meets the limit or the launcher has been opened before,
  // don't show the nudge.
  if (GetShownCount(prefs) >= kMaxShownCount ||
      WasLauncherShownPreviously(prefs)) {
    return false;
  }

  base::Time last_shown_time;
  base::TimeDelta interval;

  if (GetShownCount(prefs) == 0) {
    // Set the `last_shown_time` to the timestamp when the user first logs in
    // if the nudge hasn't been shown yet and the actual `last_shown_time` is
    // null. Calculate the expect nudge show time using that timestamp and its
    // corresponding interval.
    last_shown_time = GetFirstLoginTime(prefs);
    interval = GetNudgeInterval(/*is_first_time=*/true);
  } else {
    last_shown_time = GetLastShownTime(prefs);
    interval = GetNudgeInterval(/*is_first_time=*/false);
  }
  DCHECK(!last_shown_time.is_null());

  // The expect shown time of the nudge is set to the later one between the
  // calculated expect shown time since last shown and the
  // `earliest_available_time`, which is set to ensure the nudge is shown after
  // the home button has been shown enough of time.
  base::Time expect_shown_time =
      std::max(last_shown_time + interval, earliest_available_time_);
  if (GetNow() < expect_shown_time) {
    // Set the `recheck_time` to the expected time to show nudge.
    recheck_time = expect_shown_time;
    return false;
  }

  return true;
}

void LauncherNudgeController::HandleNudgeShown() {
  PrefService* prefs = GetPrefs();
  if (!prefs)
    return;

  const int shown_count = GetShownCount(prefs);
  ScopedDictPrefUpdate update(prefs, prefs::kShelfLauncherNudge);
  update->Set(kShownCount, shown_count + 1);
  update->Set(kLastShownTime, base::TimeToValue(GetNow()));
}

void LauncherNudgeController::MaybeShowNudge() {
  if (!features::IsShelfLauncherNudgeEnabled())
    return;

  base::Time recheck_time;
  if (!ShouldShowNudge(recheck_time)) {
    // If `recheck_time` is set, start the timer to check again later for the
    // next time to show nudge.
    if (!recheck_time.is_null())
      ScheduleShowNudgeAttempt(recheck_time);

    return;
  }

  // Don't run the nudge animation if the duration multiplier is 0 to prevent
  // crashes that caused by showing the animation that immediately gets deleted.
  if (ui::ScopedAnimationDurationScaleMode::duration_multiplier() != 0) {
    // Only show the nudge on the home button which is on the same display with
    // the cursor.
    int64_t display_id_for_nudge =
        Shell::Get()->cursor_manager()->GetDisplay().id();
    HomeButton* home_button = GetHomeButtonForDisplay(display_id_for_nudge);
    home_button->StartNudgeAnimation();
    // Only update the prefs if the nudge animation is actually shown.
    HandleNudgeShown();
  }

  // Schedule the next attempt to show nudge if the shown count hasn't hit the
  // limit after showing a nudge.
  PrefService* prefs = GetPrefs();
  if (GetShownCount(prefs) < kMaxShownCount) {
    ScheduleShowNudgeAttempt(GetLastShownTime(prefs) +
                             GetNudgeInterval(/*is_first_time=*/false));
  }
}

void LauncherNudgeController::ScheduleShowNudgeAttempt(
    base::Time recheck_time) {
  show_nudge_timer_->Start(
      FROM_HERE, recheck_time,
      base::BindOnce(&LauncherNudgeController::MaybeShowNudge,
                     weak_ptr_factory_.GetWeakPtr()));
}

void LauncherNudgeController::OnActiveUserPrefServiceChanged(
    PrefService* prefs) {
  // If the current session is a guest session which is ephemeral and doesn't
  // save prefs, return early and don't show nudges for these session types.
  if (Shell::Get()
          ->session_controller()
          ->GetUserSession(0)
          ->user_info.is_ephemeral) {
    return;
  }

  if (Shell::Get()->session_controller()->IsUserFirstLogin()) {
    // If the current logged in user is a new one, record the first login time
    // to know when to show the nudge.
    ScopedDictPrefUpdate update(prefs, prefs::kShelfLauncherNudge);
    update->Set(kFirstLoginTime, base::TimeToValue(GetNow()));
  } else if (GetFirstLoginTime(prefs).is_null()) {
    // For the users that has logged in before the nudge feature is landed, we
    // assume the user has opened the launcher before and thus don't show the
    // nudge to them.
    return;
  }

  // Set the `earliest_available_time_` according to the current login time and
  // check when the nudge could be shown.
  earliest_available_time_ = GetNow() + kMinIntervalAfterHomeButtonAppears;
  MaybeShowNudge();
}

void LauncherNudgeController::OnAppListVisibilityChanged(bool shown,
                                                         int64_t display_id) {
  PrefService* prefs = GetPrefs();
  if (!prefs)
    return;

  // App list is shown by default in tablet mode, and does not necessary
  // require explicit user action. As a result, don't track app list visibility
  // changes in tablet mode as actions affecting nudge availability in clamshell
  // mode.
  if (Shell::Get()->IsInTabletMode())
    return;

  if (!WasLauncherShownPreviously(prefs) && shown) {
    ScopedDictPrefUpdate update(prefs, prefs::kShelfLauncherNudge);
    update->Set(kWasLauncherShown, true);
  }
}

void LauncherNudgeController::OnDisplayTabletStateChanged(
    display::TabletState state) {
  if (state != display::TabletState::kInClamshellMode) {
    return;
  }

  // If a nudge event became available while the device was in tablet mode, it
  // would have been ignored. Recheck whether the nudge can be shown again. Note
  // that the nudge is designed to be shown after
  // `kMinIntervalAfterHomeButtonAppears` amount of time since changing to
  // clamshell mode where home button exists.
  earliest_available_time_ = GetNow() + kMinIntervalAfterHomeButtonAppears;
  MaybeShowNudge();
}

base::Time LauncherNudgeController::GetNow() const {
  if (clock_for_test_)
    return clock_for_test_->Now();

  return base::Time::Now();
}

}  // namespace ash