| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/hats/hats_dialog.h" |
| |
| #include <string_view> |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/containers/flat_map.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/hats/hats_config.h" |
| #include "chrome/browser/ash/hats/hats_finch_helper.h" |
| #include "chrome/browser/profiles/profile_destroyer.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/browser_dialogs.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/browser_resources.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chromeos/version/version_loader.h" |
| #include "components/language/core/browser/pref_names.h" |
| #include "components/language/core/common/locale_util.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/version_info/version_info.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "third_party/re2/src/re2/re2.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/mojom/ui_base_types.mojom-shared.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| using content::WebContents; |
| using content::WebUIMessageHandler; |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Default width/height ratio of screen size. |
| const int kDefaultWidth = 384; |
| const int kDefaultHeight = 428; |
| |
| // The state specific UMA enumerations |
| const int kSurveyDisplayedEnumeration = 2; |
| const int kSurveyCompleteEnumeration = 3; |
| |
| // Possible requested actions from the HTML+JS client. |
| // Client is ready to close the page. |
| const char kClientActionLoad[] = "load"; |
| // Client is ready to close the page. |
| const char kClientActionClose[] = "close"; |
| // Client is ready to close the page after completing the survey. |
| const char kClientActionComplete[] = "complete"; |
| // There was an unhandled error and we need to log and close the page. |
| const char kClientActionUnhandledError[] = "survey-loading-error"; |
| // A smiley was selected, so we'd like to track that. |
| const char kClientQuestionAnswered[] = "answer-"; |
| const char kClientQuestionAnsweredRegex[] = "answer-(\\d+)-((?:\\d+,?)+)"; |
| const char kClientQuestionAnsweredScoreRegex[] = "(\\d+),?"; |
| |
| constexpr char kCrOSHaTSURL[] = |
| "https://storage.googleapis.com/chromeos-hats-web-stable/index.html"; |
| |
| } // namespace |
| |
| // Only log a histogram value if there is a histogram name provided. |
| void LogHistogram(const std::string& histogram_name, int enumeration) { |
| if (!histogram_name.empty()) { |
| base::UmaHistogramSparse(histogram_name, enumeration); |
| } |
| } |
| |
| // static |
| bool HatsDialog::ParseAnswer(const std::string& input, |
| int* question, |
| std::vector<int>* scores) { |
| std::string question_num_string; |
| std::string_view all_scores_string; |
| if (!RE2::FullMatch(input, kClientQuestionAnsweredRegex, &question_num_string, |
| &all_scores_string)) |
| return false; |
| |
| if (!base::StringToInt(question_num_string, question) || *question <= 0 || |
| *question > 10) { |
| LOG(ERROR) << "Can't parse Survey score"; |
| return false; |
| } |
| |
| std::string score_string; |
| while (RE2::FindAndConsume( |
| &all_scores_string, kClientQuestionAnsweredScoreRegex, &score_string)) { |
| int score; |
| if (!base::StringToInt(score_string, &score) || score <= 0 || score > 100) { |
| LOG(ERROR) << "Can't parse Survey score"; |
| return false; |
| } |
| scores->push_back(score); |
| } |
| |
| return true; |
| } |
| |
| bool HatsDialog::HandleClientTriggeredAction( |
| const std::string& action, |
| const std::string& histogram_name) { |
| DVLOG(1) << "HandleClientTriggeredAction: Received " << action; |
| |
| // Page asks to be closed. |
| if (action == kClientActionClose) { |
| return true; |
| } |
| |
| // An unhandled error in our client, log and close. |
| if (base::StartsWith(action, kClientActionUnhandledError)) { |
| LOG(ERROR) << "Error while loading a HaTS Survey " << action; |
| return true; |
| } |
| |
| // Page successfully loaded the survey. |
| if (action == kClientActionLoad) { |
| LogHistogram(histogram_name, kSurveyDisplayedEnumeration); |
| return false; |
| } |
| |
| // Page asks to be closed after completing the survey. |
| if (action == kClientActionComplete) { |
| LogHistogram(histogram_name, kSurveyCompleteEnumeration); |
| return true; |
| } |
| |
| // A question was answered |
| if (base::StartsWith(action, kClientQuestionAnswered)) { |
| int question; |
| std::vector<int> question_scores; |
| if (!ParseAnswer(action, &question, &question_scores)) { |
| return false; // It's a client error, but don't close the page. |
| } |
| |
| for (int score : question_scores) { |
| // The enumeration is specified as `QQNN`, where `QQ` is the question |
| // number and `NN` is the answer index. Therefore, we can calculate this |
| // value via `QQ * 100 + NN`. |
| // Note: The `ParseAnswer` function guarantees that the score will be |
| // in the range [1, 100]. |
| int enumeration = (question * 100) + score; |
| LogHistogram(histogram_name, enumeration); |
| } |
| |
| return false; // Don't close the page. |
| } |
| |
| // Future proof - ignore unimplemented commands. |
| return false; |
| } |
| |
| HatsDialog::HatsDialog(const std::string& trigger_id, |
| const std::string& histogram_name, |
| const std::string& site_context) |
| : trigger_id_(trigger_id), histogram_name_(histogram_name) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| set_allow_default_context_menu(false); |
| set_can_close(true); |
| set_can_resize(false); |
| set_dialog_content_url(GURL(std::string(kCrOSHaTSURL) + "?emitAnswers=true&" + |
| site_context + "&trigger=" + trigger_id_)); |
| set_dialog_frame_kind(ui::WebDialogDelegate::FrameKind::kDialog); |
| set_dialog_modal_type(ui::mojom::ModalType::kSystem); |
| set_dialog_size(gfx::Size(kDefaultWidth, kDefaultHeight)); |
| set_show_close_button(true); |
| set_show_dialog_title(false); |
| } |
| |
| HatsDialog::~HatsDialog() = default; |
| |
| void HatsDialog::Show(const std::string& trigger_id, |
| const std::string& histogram_name, |
| const std::string& site_context) { |
| // HatsDialog is self-deleting via OnDialogClosed(). |
| chrome::ShowWebDialog( |
| nullptr, ProfileManager::GetActiveUserProfile(), |
| new HatsDialog(trigger_id, histogram_name, site_context)); |
| } |
| |
| void HatsDialog::OnLoadingStateChanged(WebContents* source) { |
| // Only trigger actions when the URL changes |
| if (action_ != source->GetURL().GetRef()) { |
| action_ = source->GetURL().GetRef(); |
| if (HandleClientTriggeredAction(action_, histogram_name_)) { |
| source->ClosePage(); |
| } |
| } |
| } |
| |
| } // namespace ash |