blob: 50a839e18777b628b7cb5ac0fab2f80ef20b94f3 [file] [log] [blame]
// Copyright 2021 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 "components/user_education/common/tutorial_service.h"
#include <memory>
#include <vector>
#include "base/auto_reset.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 {
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) {
toggle_focus_subscription_ =
help_bubble_factory_registry->AddToggleFocusCallback(
base::BindRepeating(&TutorialService::OnFocusToggledForAccessibility,
base::Unretained(this)));
}
TutorialService::~TutorialService() = default;
bool TutorialService::StartTutorial(TutorialIdentifier id,
ui::ElementContext context,
CompletedCallback completed_callback,
AbortedCallback aborted_callback) {
// Overriding an existing running tutorial is not supported. In this case
// return false to the caller.
if (running_tutorial_)
return false;
// Get the description from the tutorial registry.
TutorialDescription* description =
tutorial_registry_->GetTutorialDescription(id);
CHECK(description);
// Construct the tutorial from the dsecription.
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);
// Start the tutorial and mark the params used to created it for restarting.
running_tutorial_->Start();
toggle_focus_count_ = 0;
return true;
}
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);
}
bool TutorialService::RestartTutorial() {
DCHECK(running_tutorial_ && running_tutorial_creation_params_);
base::AutoReset<bool> resetter(&is_restarting_, true);
currently_displayed_bubble_.reset();
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_)
return CompleteTutorial();
if (abort_step.has_value())
running_tutorial_creation_params_->description_->histograms
->RecordAbortStep(abort_step.value());
// Log the failure of completion for the tutorial.
if (running_tutorial_creation_params_->description_->histograms)
running_tutorial_creation_params_->description_->histograms->RecordComplete(
false);
UMA_HISTOGRAM_BOOLEAN("Tutorial.Completion", false);
// Reset the tutorial and call the external abort callback.
ResetRunningTutorial();
// Record how many times the user toggled focus during the tutorial using
// the keyboard.
UMA_HISTOGRAM_CUSTOM_COUNTS("Tutorial.FocusToggleCount.Aborted",
toggle_focus_count_, 0, 50, 6);
toggle_focus_count_ = 0;
if (aborted_callback_) {
std::move(aborted_callback_).Run();
}
}
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();
// Record how many times the user toggled focus during the tutorial using
// the keyboard.
UMA_HISTOGRAM_CUSTOM_COUNTS("Tutorial.FocusToggleCount.Completed",
toggle_focus_count_, 0, 50, 6);
toggle_focus_count_ = 0;
std::move(completed_callback_).Run();
}
void TutorialService::SetCurrentBubble(std::unique_ptr<HelpBubble> bubble) {
DCHECK(running_tutorial_);
currently_displayed_bubble_ = std::move(bubble);
}
void TutorialService::HideCurrentBubbleIfShowing() {
if (currently_displayed_bubble_) {
currently_displayed_bubble_.reset();
}
}
bool TutorialService::IsRunningTutorial() const {
return running_tutorial_ != nullptr;
}
void TutorialService::ResetRunningTutorial() {
DCHECK(running_tutorial_);
running_tutorial_.reset();
running_tutorial_creation_params_.reset();
running_tutorial_was_restarted_ = false;
currently_displayed_bubble_.reset();
}
void TutorialService::OnFocusToggledForAccessibility(HelpBubble* bubble) {
if (bubble == currently_displayed_bubble_.get())
++toggle_focus_count_;
}
} // namespace user_education