chromium/chrome/browser/ash/hats/hats_dialog.cc

// 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_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 "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().ref()) {
    action_ = source->GetURL().ref();
    if (HandleClientTriggeredAction(action_, histogram_name_)) {
      source->ClosePage();
    }
  }
}

}  // namespace ash