blob: fde1ebc56cc7fc2ec5543a3c95859a72f61ee14e [file] [log] [blame]
// 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 "components/user_education/common/tutorial_service.h"
#include <memory>
#include <vector>
#include "base/auto_reset.h"
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/help_bubble_factory_registry.h"
#include "components/user_education/common/tutorial.h"
#include "components/user_education/common/tutorial_identifier.h"
#include "components/user_education/common/tutorial_registry.h"
namespace user_education {
namespace {
// How long a tutorial has to go without a bubble before we assume it's broken
// and abort it.
constexpr base::TimeDelta kBrokenTutorialTimeout = base::Seconds(15);
// How long a tutorial has to go before the first bubble is shown before we
// assume it's been broken or abandoned and abort it. This is longer than the
// above because we want to allow the user time to navigate to the surface that
// triggers the tutorial.
constexpr base::TimeDelta kTutorialNotStartedTimeout = base::Seconds(60);
} // namespace
TutorialService::TutorialCreationParams::TutorialCreationParams(
TutorialDescription* description,
ui::ElementContext context)
: description_(description), context_(context) {}
TutorialService::TutorialService(
TutorialRegistry* tutorial_registry,
HelpBubbleFactoryRegistry* help_bubble_factory_registry)
: tutorial_registry_(tutorial_registry),
help_bubble_factory_registry_(help_bubble_factory_registry) {}
TutorialService::~TutorialService() = default;
void TutorialService::StartTutorial(TutorialIdentifier id,
ui::ElementContext context,
CompletedCallback completed_callback,
AbortedCallback aborted_callback) {
// End the current tutorial, if any.
if (running_tutorial_) {
if (is_final_bubble_) {
// The current tutorial is showing the final congratulatory bubble, so it
// is effectively complete.
CompleteTutorial();
} else {
running_tutorial_->Abort();
}
}
is_final_bubble_ = false;
// Get the description from the tutorial registry.
TutorialDescription* description =
tutorial_registry_->GetTutorialDescription(id);
CHECK(description);
// Construct the tutorial from the description.
running_tutorial_ =
Tutorial::Builder::BuildFromDescription(*description, this, context);
// Set the external callbacks.
completed_callback_ = std::move(completed_callback);
aborted_callback_ = std::move(aborted_callback);
// Save the params for creating the tutorial to be used when restarting.
running_tutorial_creation_params_ =
std::make_unique<TutorialCreationParams>(description, context);
// Before starting the tutorial, set a timeout just in case the user never
// actually gets to a place where they can launch the first bubble.
broken_tutorial_timer_.Start(
FROM_HERE, kTutorialNotStartedTimeout,
base::BindOnce(&TutorialService::OnBrokenTutorial,
base::Unretained(this)));
// Start the tutorial and mark the params used to created it for restarting.
running_tutorial_->Start();
}
void TutorialService::LogIPHLinkClicked(TutorialIdentifier id,
bool iph_link_was_clicked) {
TutorialDescription* description =
tutorial_registry_->GetTutorialDescription(id);
CHECK(description);
if (description->histograms)
description->histograms->RecordIphLinkClicked(iph_link_was_clicked);
}
void TutorialService::LogStartedFromWhatsNewPage(TutorialIdentifier id,
bool success) {
TutorialDescription* description =
tutorial_registry_->GetTutorialDescription(id);
CHECK(description);
if (description->histograms)
description->histograms->RecordStartedFromWhatsNewPage(success);
}
bool TutorialService::RestartTutorial() {
DCHECK(running_tutorial_ && running_tutorial_creation_params_);
base::AutoReset<bool> resetter(&is_restarting_, true);
HideCurrentBubbleIfShowing();
running_tutorial_ = Tutorial::Builder::BuildFromDescription(
*running_tutorial_creation_params_->description_, this,
running_tutorial_creation_params_->context_);
if (!running_tutorial_) {
ResetRunningTutorial();
return false;
}
// Note: if we restart the tutorial, we won't record whether the user pressed
// the pane focus key to focus the help bubble until the user actually decides
// they're finished, but we also won't reset the count, so at the end we can
// record the total.
// TODO(dfried): decide if this is actually the correct behavior.
running_tutorial_was_restarted_ = true;
running_tutorial_->Start();
return true;
}
void TutorialService::AbortTutorial(absl::optional<int> abort_step) {
// For various reasons, we could get called here while e.g. tearing down the
// interaction sequence. We only want to actually run AbortTutorial() or
// CompleteTutorial() exactly once, so we won't continue if the tutorial has
// already been disposed. We also only want to abort the tutorial if we are
// not in the process of restarting. When calling reset on the help bubble,
// or when resetting the tutorial, the interaction sequence or callbacks could
// call the abort.
if (!running_tutorial_ || is_restarting_)
return;
// If the tutorial had been restarted and then aborted, The tutorial should be
// considered completed.
if (running_tutorial_was_restarted_) {
CompleteTutorial();
return;
}
if (running_tutorial_creation_params_->description_->histograms) {
if (abort_step.has_value()) {
running_tutorial_creation_params_->description_->histograms
->RecordAbortStep(abort_step.value());
}
running_tutorial_creation_params_->description_->histograms->RecordComplete(
false);
}
UMA_HISTOGRAM_BOOLEAN("Tutorial.Completion", false);
// Reset the tutorial and call the external abort callback.
ResetRunningTutorial();
if (aborted_callback_) {
std::move(aborted_callback_).Run();
}
}
void TutorialService::OnNonFinalBubbleClosed(HelpBubble* bubble) {
if (bubble != currently_displayed_bubble_.get()) {
return;
}
bubble_closed_subscription_ = base::CallbackListSubscription();
currently_displayed_bubble_.reset();
broken_tutorial_timer_.Start(
FROM_HERE, kBrokenTutorialTimeout,
base::BindOnce(&TutorialService::OnBrokenTutorial,
base::Unretained(this)));
}
void TutorialService::CompleteTutorial() {
DCHECK(running_tutorial_);
// Log the completion metric based on if the tutorial was restarted or not.
if (running_tutorial_creation_params_->description_->histograms)
running_tutorial_creation_params_->description_->histograms->RecordComplete(
true);
UMA_HISTOGRAM_BOOLEAN("Tutorial.Completion", true);
ResetRunningTutorial();
std::move(completed_callback_).Run();
}
void TutorialService::SetCurrentBubble(std::unique_ptr<HelpBubble> bubble,
bool is_last_step) {
DCHECK(running_tutorial_);
currently_displayed_bubble_ = std::move(bubble);
broken_tutorial_timer_.Stop();
if (is_last_step) {
is_final_bubble_ = true;
bubble_closed_subscription_ =
currently_displayed_bubble_->AddOnCloseCallback(base::BindOnce(
[](TutorialService* service, user_education::HelpBubble*) {
service->CompleteTutorial();
},
base::Unretained(this)));
} else {
is_final_bubble_ = false;
bubble_closed_subscription_ =
currently_displayed_bubble_->AddOnCloseCallback(base::BindOnce(
&TutorialService::OnNonFinalBubbleClosed, base::Unretained(this)));
}
}
void TutorialService::HideCurrentBubbleIfShowing() {
if (!currently_displayed_bubble_)
return;
bubble_closed_subscription_ = base::CallbackListSubscription();
currently_displayed_bubble_.reset();
}
bool TutorialService::IsRunningTutorial() const {
return running_tutorial_ != nullptr;
}
void TutorialService::ResetRunningTutorial() {
DCHECK(running_tutorial_);
broken_tutorial_timer_.Stop();
running_tutorial_.reset();
running_tutorial_creation_params_.reset();
running_tutorial_was_restarted_ = false;
HideCurrentBubbleIfShowing();
}
void TutorialService::OnBrokenTutorial() {
if (running_tutorial_ && !currently_displayed_bubble_) {
running_tutorial_->Abort();
}
}
} // namespace user_education