chromium/ash/wm/float/tablet_mode_tuck_education.cc

// 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 "ash/wm/float/tablet_mode_tuck_education.h"

#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/rounded_label.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/property_change_reason.h"
#include "ui/display/display.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

// The vertical distance from the window header bar to the nudge.
constexpr int kNudgeYOffset = 8;
// The time it takes for the nudge to fade in or out.
constexpr base::TimeDelta kNudgeFadeDuration = base::Milliseconds(100);
// The time after the second bounce finishes until the nudge starts to fade
// out.
constexpr base::TimeDelta kNudgeFadeOutDelay = base::Seconds(1);

// The maximum distance the window translates by during the bounce.
constexpr float kBounceDistance = 50.0f;
// The delay before each bounce.
constexpr base::TimeDelta kBounceDelay = base::Seconds(1);
// The time for the window to move from its starting position to its furthest
// position.
constexpr base::TimeDelta kBounceStartDuration = base::Milliseconds(400);
// The time for the window to move from its furthest position to back to its
// original position.
constexpr base::TimeDelta kBounceEndDuration = base::Milliseconds(700);

// RoundedLabel construction values.
constexpr int kLabelHorizontalPadding = 16;
constexpr int kLabelHeight = 36;
constexpr float kRoundedDivisor = 2.f;

// The nudge will not be shown if it already been shown 3 times, or if 24 hours
// have not yet passed since it was last shown.
constexpr int kNudgeMaxShownCount = 3;
constexpr base::TimeDelta kNudgeTimeBetweenShown = base::Hours(24);

// The name of an integer pref that counts the number of times we have shown
// the nudge.
const char kTuckEducationShownCount[] =
    "ash.wm_nudge.tuck_education_nudge_count";
// The name of a time pref that stores the time we last showed the nudge.
const char kTuckEducationLastShown[] =
    "ash.wm_nudge.tuck_education_nudge_last_shown";

// Clock that can be overridden for testing.
base::Clock* g_clock_override = nullptr;

base::Time GetTime() {
  return g_clock_override ? g_clock_override->Now() : base::Time::Now();
}

std::unique_ptr<views::Widget> CreateWidget(aura::Window* window) {
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.name = "TuckEducationNudgeWidget";
  params.accept_events = false;
  params.parent = window;
  params.child = true;

  auto widget = std::make_unique<views::Widget>(std::move(params));
  widget->GetLayer()->SetFillsBoundsOpaquely(false);

  auto nudge_label = std::make_unique<views::Label>(
      l10n_util::GetStringUTF16(IDS_ASH_TUCK_EDUCATIONAL_NUDGE_LABEL));
  nudge_label->SetBackground(views::CreateThemedRoundedRectBackground(
      ui::kColorSysSurface3, kLabelHeight / kRoundedDivisor));
  nudge_label->SetPreferredSize(gfx::Size(
      nudge_label->GetPreferredSize().width() + kLabelHorizontalPadding * 2,
      kLabelHeight));

  widget->SetContentsView(std::move(nudge_label));

  return widget;
}

}  // namespace

TabletModeTuckEducation::TabletModeTuckEducation(aura::Window* floated_window) {
  // Observe for end of floating crossfade animation to begin education.
  window_ = floated_window;
  window_observation_.Observe(window_.get());
}

TabletModeTuckEducation::~TabletModeTuckEducation() = default;

// static
void TabletModeTuckEducation::RegisterProfilePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(kTuckEducationShownCount, 0);
  registry->RegisterTimePref(kTuckEducationLastShown, base::Time());
}

// static
bool TabletModeTuckEducation::CanActivateTuckEducation() {
  auto* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  CHECK(pref_service);
  // Nudge has already been shown three times. No need to educate anymore.
  if (pref_service->GetInteger(kTuckEducationShownCount) >=
      kNudgeMaxShownCount) {
    return false;
  }

  // Nudge has been shown within the last 24 hours already.
  if (GetTime() - pref_service->GetTime(kTuckEducationLastShown) <
      kNudgeTimeBetweenShown) {
    return false;
  }

  return true;
}

// static
void TabletModeTuckEducation::OnWindowTucked() {
  auto* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  // Set the count to the maximum value so the education doesn't show anymore.
  pref_service->SetInteger(kTuckEducationShownCount, kNudgeMaxShownCount);
}

void TabletModeTuckEducation::OnWindowTransformed(
    aura::Window* window,
    ui::PropertyChangeReason reason) {
  // Floating a window causes a crossfade animation that can interfere with
  // other animations or actions performed on the window at the same time. We
  // observe for the crossfade to finish before performing the bounce.
  bool animating = window->layer()->GetAnimator()->IsAnimatingProperty(
      ui::LayerAnimationElement::TRANSFORM);
  if (!nudge_widget_ && !animating &&
      reason == ui::PropertyChangeReason::FROM_ANIMATION) {
    ActivateTuckEducation();
  }
}

void TabletModeTuckEducation::ActivateTuckEducation() {
  // No need to observe for the transform animation (crossfade) to finish as
  // soon as it has happened once.
  window_observation_.Reset();

  nudge_widget_ = CreateWidget(window_);
  nudge_widget_->Show();

  auto* nudge_layer = nudge_widget_->GetLayer();
  nudge_layer->SetOpacity(0.0f);

  const gfx::Size nudge_pref_size =
      nudge_widget_->GetContentsView()->GetPreferredSize();
  const gfx::Rect window_bounds = window_->GetBoundsInScreen();

  nudge_widget_->SetBounds(gfx::Rect(
      (window_bounds.width() - nudge_pref_size.width()) / 2,
      window_->GetProperty(aura::client::kTopViewInset) + kNudgeYOffset,
      nudge_pref_size.width(), nudge_pref_size.height()));

  // Move window towards edge of screen.
  const display::Display display =
      display::Screen::GetScreen()->GetDisplayNearestWindow(window_);
  const bool bounce_right =
      window_bounds.CenterPoint().x() > display.bounds().CenterPoint().x();
  const gfx::Transform side_transform = gfx::Transform::MakeTranslation(
      bounce_right ? kBounceDistance : -kBounceDistance, 0);

  // Start nudge at the top of the window (not animated).
  const gfx::Transform nudge_start_transform = gfx::Transform::MakeTranslation(
      0, -window_->GetProperty(aura::client::kTopViewInset));

  // Move the window back to its starting position, and the nudge to just below
  // the header bar.
  const gfx::Transform reset_transform = gfx::Transform();

  views::AnimationBuilder()
      .OnAborted(base::BindOnce(&TabletModeTuckEducation::DismissNudge,
                                weak_factory_.GetWeakPtr()))
      .OnEnded(base::BindOnce(&TabletModeTuckEducation::DismissNudge,
                              weak_factory_.GetWeakPtr()))
      .SetPreemptionStrategy(ui::LayerAnimator::ENQUEUE_NEW_ANIMATION)
      // Set initial nudge position at the top of the window.
      .Once()
      .SetTransform(nudge_layer, nudge_start_transform)
      // Fade nudge in and drop down to actual position.
      .Then()
      .SetDuration(kNudgeFadeDuration)
      .SetOpacity(nudge_layer, 1.0f, gfx::Tween::LINEAR)
      .SetTransform(nudge_layer, reset_transform)
      // Delay before first bounce.
      .Then()
      .SetDuration(kBounceDelay)
      // First bounce.
      .Then()
      .SetDuration(kBounceStartDuration)
      .SetTransform(window_, side_transform, gfx::Tween::ACCEL_20_DECEL_100)
      .Then()
      .SetDuration(kBounceEndDuration)
      .SetTransform(window_, reset_transform, gfx::Tween::ACCEL_20_DECEL_100)
      // Delay before second bounce.
      .Then()
      .SetDuration(kBounceDelay)
      // Second bounce.
      .Then()
      .SetDuration(kBounceStartDuration)
      .SetTransform(window_, side_transform, gfx::Tween::ACCEL_20_DECEL_100)
      .Then()
      .SetDuration(kBounceEndDuration)
      .SetTransform(window_, reset_transform, gfx::Tween::ACCEL_20_DECEL_100)
      // Delay before fading out.
      .Then()
      .SetDuration(kNudgeFadeOutDelay)
      // Fade nudge out.
      .Then()
      .SetDuration(kNudgeFadeDuration)
      .SetOpacity(nudge_layer, 0.0f, gfx::Tween::LINEAR);

  // Update the preferences.
  auto* pref_service =
      Shell::Get()->session_controller()->GetActivePrefService();
  pref_service->SetInteger(
      kTuckEducationShownCount,
      pref_service->GetInteger(kTuckEducationShownCount) + 1);
  pref_service->SetTime(kTuckEducationLastShown, base::Time());
}

void TabletModeTuckEducation::DismissNudge() {
  window_ = nullptr;

  if (nudge_widget_ && !nudge_widget_->IsClosed()) {
    nudge_widget_->GetLayer()->GetAnimator()->AbortAllAnimations();
    nudge_widget_->CloseNow();
  }
}

// static
void TabletModeTuckEducation::SetOverrideClockForTesting(
    base::Clock* test_clock) {
  g_clock_override = test_clock;
}

}  // namespace ash